天天看點

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

目錄

  • 前言
  • Model 類的小小改動
  • phong 光照模型
    • phong 光照簡介
    • 環境光
    • 漫反射
    • 高光
  • 在着色器中實作 phong 光照
  • 完整代碼
    • c++
    • 着色器

前言

上一篇回顧:OpenGL學習(七)通過assimp庫讀取多種格式的模型

在上一篇部落格中,我們實作了最簡單的網格對象 Mesh,并且從 assimp 庫接收我們需要的資訊,同時進行繪制。這意味着我們逐漸步入現代。

今天我們要利用 phong 光照模型,實作對物體的光照效果的模拟,讓場景更加真實。

注:

本篇部落格代碼基于上一篇部落格:OpenGL學習(七)通過assimp庫讀取多種格式的模型

Model 類的小小改動

在開始之前,我們要對我們昨天(上一篇部落格)新鮮封裝的 model 類進行一些改動。在昨天,我們直接傳遞了一個模型變換矩陣 model 到着色器,因為我們隻繪制一個物體,足夠了。

希望你不會發現上一篇部落格我偷懶了,這本來就應該是在 Model 類定義的時候應該完成的。。。

如果要繪制多個不同的物體,那麼問題來了。不同的物體我們需要傳遞不同的模型變換矩陣,于是我們把這一步驟放到了 Model 類的 draw 函數中。

我們添加三個成員,表示一個模型對象的平移,旋轉,縮放:

class Model
{
public:
	// ...
    glm::vec3 translate=glm::vec3(0,0,0), rotate = glm::vec3(0, 0, 0), scale = glm::vec3(1, 1, 1);
    // ...
           

緊接着我們在 draw 函數中,繪制之前,傳遞本模型的模型變換矩陣即可:

// 傳模型矩陣
glm::mat4 unit(    // 機關矩陣
    glm::vec4(1, 0, 0, 0),
    glm::vec4(0, 1, 0, 0),
    glm::vec4(0, 0, 1, 0),
    glm::vec4(0, 0, 0, 1)
);
glm::mat4 scale = glm::scale(unit, this->scale);
glm::mat4 translate = glm::translate(unit, this->translate);

glm::mat4 rotate = unit;    // 旋轉
rotate = glm::rotate(rotate, glm::radians(this->rotate.x), glm::vec3(1, 0, 0)); 
rotate = glm::rotate(rotate, glm::radians(this->rotate.y), glm::vec3(0, 1, 0));  
rotate = glm::rotate(rotate, glm::radians(this->rotate.z), glm::vec3(0, 0, 1));

// 模型變換矩陣
glm::mat4 model = translate * rotate * scale;
GLuint mlocation = glGetUniformLocation(program, "model");    // 名為model的uniform變量的位置索引
glUniformMatrix4fv(mlocation, 1, GL_FALSE, glm::value_ptr(model));   // 列優先矩陣
           

遇到任何問題?回顧之前的部落格:OpenGL學習(三)三維繪制與模型變換矩陣

對于多個模型,我們建立一個全局 vector 變量,名叫 models,她存儲不同的 Model 對象。

我們在初始化的時候,就應該指定模型的平移旋轉縮放參數,同時将 Model 對象加入 models :

Model tree1 = Model();
tree1.translate = glm::vec3(0.25, 0, -1);
tree1.scale = glm::vec3(0.00025, 0.00025, 0.00025);
tree1.load("models/tree/tree02.obj");
models.push_back(tree1);
           

在 display 中,我們直接繪制所有的 Model 對象即可:

for (auto m : models)
{
    m.draw(program);
}
           

我們現在可以自由的繪制多個模型:

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

phong 光照模型

在現實世界中,光線從各種光源出發,經過無數次反彈,最終進入眼鏡。這種現象對于計算機來說幾乎不可解,因為複雜度高的一。

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

時下流行的方法是從錄影機方向,逐像素,向場景中投射(一條或者多條)光線,光線沿途不斷反彈,每次反彈都搜集資訊(比如碰撞點的顔色),直到滿足某些條件就終止。常用的方法有蒙卡洛特路徑追蹤,輻射度方法等。這些方法統稱為光線追蹤,簡稱光追。

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

光線追蹤最大的難點就是求交,即目前光線何時碰撞到實體?碰撞的實體顔色是什麼?此外,光追需要大量的疊代才能夠拟合,這意味着我們要向一個像素發送若幹條光線(128,256,甚至更多)才能夠達到不錯的效果,計算量也是一大難點!

至于求交等大量計算的工作,圖形界的大佬們提出了體積樹(BVH)的方式來進行空間劃分求交,而老黃等一衆顯示卡開發商則将這些操作 “焊死” 在顯示卡的內建電路和驅動程式中,就如同 OpenGL 的裁剪,光栅化等操作。于是乎,RTX 系顯示卡就會有一些單元叫做 “光追單元”,這個和經典顯示卡的幾何單元,光栅化單元異曲同工。。。

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

我們可以注意到了,這些新式的東西,并不在 OpenGL 流水線的範疇内。這意味着我們隻有使用其他的圖形 API(比如 Vulkan)才能有機會使用這些新東東。唔,OpenGL 畢竟是上個時代的了(霧)

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

如果你讀過我之前寫的這篇部落格:從零開始編寫minecraft光影包(9)進階水面繪制 反射與螢幕空間反射,你可能會說了,我們不是已經實作了光線追蹤嘛?

其實這種叫做螢幕空間光線追蹤,它的資訊都來自于我們的螢幕空間,換句話說,我們隻能記錄那些我們看到的東西,這也是傳統的 OpenGL 流水線限制造成的。對于螢幕空間外的資訊我們一無所知!

那麼傳統的 OpenGL 流水線有沒有光追呢?有!Minecraft 光影 SEUS 的作者 SE 大佬就實作了。至于天才 SE 的 PTGI 是怎麼記錄螢幕空間外的資訊,唔。。。我不清楚,估計是用了 shadow 陰影幀緩沖的顔色附件?畢竟這是唯一我能夠想到的擷取螢幕空間外資訊的方式

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

回想我們玩遊戲的時候,螢幕空間外的物品,仍然會将陰影投影到我們的螢幕上。這樣一解釋就合理了。

啊啊啊啊扯遠了扯遠了,我爬我爬 dbq Orz 咚咚咚

phong 光照簡介

因為經典的全局光照模型太過複雜,而且對于 OpenGL 流水線來說非常難實作,于是早些年的圖形程式員提出了一個簡單的模型,叫做 phong 光照模型,該模型能夠以極低的代價模拟真實的光照場景,在計算機遊戲實時渲染領域是成本效益極高的模型。直到現在,很多計算機遊戲仍然是沿用這一套模型。

phong 光照模型将物體的光線分為三大類,分别是:

  1. 環境光 ambient
  2. 漫反射光 diffuse
  3. 鏡面高光 specular

其中環境光 ambient 是一個固定的數值,漫反射光 diffuse 和光源的角度,物體的法向量有關,而鏡面高光則和 specular 視線方向,光源角度有關。

最後的光照總和可以用如下的公式簡單的描述:

l i g h t = a m b i e n t + d i f f u s e + s p e c u l a r light = ambient + diffuse + specular light=ambient+diffuse+specular

環境光

其中環境光 ambient 是一個固定的數值,物體的每一個像素衆生平等。環境光是為了模拟那些經過 n 次反射的光,就如同你半夜起來上廁所,周圍不是全黑的,通常是有一絲亮度的,這就是環境光。

環境光的計算十分簡單:

a m b i e n t = K a ambient = Ka ambient=Ka

其中 Ka 是環境光的系數,由物體的材質決定。

漫反射

而漫反射光則需要考慮光源和物體的位置關系了。根據實體定律,光線直射物體的時候,反射的光最多,而光線平視物體的時候,我們幾乎無法接收到反射光。

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

假設光線方向為 L,L’ 為光線的反方向,法線方向為 N,我們可以得出,當光線垂直攝入時,即 N 和 L 成 180 度,即 N 和 L’ 成 0 度時,最亮!

于是有:

d i f f u s e = c o s ( t h e t a ) ∗ K d = d o t ( − L , N ) ∗ K d diffuse = cos(theta) * Kd = dot(-L, N) * Kd diffuse=cos(theta)∗Kd=dot(−L,N)∗Kd

其中 -L 是 L’ 即入射光的反方向,而 Kd 則是材質的漫反射系數。

高光

高光的原理是反射光線之後,大部分光線都會位于反射光線附近,這部分會高亮過其他地方:

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

如何判斷高光什麼時候入眼呢?我們視線方向,和反射光線的方向越近(θ 越小),就能夠看到越多的高光!

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

注:

這裡取的是反射光線的反方向!

我們令反射光線為 R,視線方向為 V,于是有高光公式:

s p e c u l a r = c o s ( t h e t a ) ∗ K s = d o t ( − R , V ) ∗ K s specular = cos(theta) * Ks = dot(-R, V) * Ks specular=cos(theta)∗Ks=dot(−R,V)∗Ks

其中我們給 cos(θ) 做一個指數,即 cos 的 a 次方,其中 a 是高光衰減系數。是以最終有:

k s cos ⁡ α ϕ \boldsymbol{k}_{s}\cos ^{\alpha} \boldsymbol{\phi} ks​cosαϕ

在着色器中實作 phong 光照

我們直接套公式即可:首先在頂點着色器,我們擷取法線和坐标并且 pass 到片段着色器。注意法線直接乘以模型矩陣,是不完全正确的。在等軸縮放的時候,我們可以這麼用!

頂點着色器:

#version 330 core

// 頂點着色器輸入
layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec2 vTexcoord;
layout (location = 2) in vec3 vNormal;

out vec3 worldPos;
out vec2 texcoord;
out vec3 normal;

uniform mat4 model;         // 模型變換矩陣
uniform mat4 view;          // 模型變換矩陣
uniform mat4 projection;    // 模型變換矩陣

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

    // 傳遞到片段着色器
    texcoord = vTexcoord;   
    worldPos = (model * vec4(vPosition, 1.0)).xyz;
    normal = (model * vec4(vNormal, 0.0)).xyz;
}
           

在片段着色器中,我們編寫一個函數,計算光照系數,其中第參數是:像素的世界坐标,錄影機位置,光源位置,像素的法線在世界空間下的方向。然後我們代公式即可。

片段着色器:

#version 330 core

in vec3 worldPos;   // 目前片元的世界坐标
in vec2 texcoord;   // 紋理坐标
in vec3 normal;     // 法向量

out vec4 fColor;    // 片元輸出像素的顔色

uniform sampler2D texture;  // 紋理圖檔

uniform vec3 lightPos;  // 光源位置
uniform vec3 cameraPos; // 相機位置

float phong(vec3 worldPos, vec3 cameraPos, vec3 lightPos, vec3 normal)
{
    vec3 N = normalize(normal);
    vec3 V = normalize(worldPos - cameraPos);
    vec3 L = normalize(worldPos - lightPos);
    vec3 R = reflect(L, N);

    float ambient = 0.3;
    float diffuse = max(dot(N, -L), 0) * 0.7;
    float specular = pow(max(dot(-R, V), 0), 50.0) * 1.1;

    return ambient + diffuse + specular;
}

void main()
{
    fColor.rgb =  texture2D(texture, texcoord).rgb;
    fColor.rgb *= phong(worldPos, cameraPos, lightPos, normal);
}
           

别忘了在 c++ 代碼中,我們操控光源。我們傳送光源位置和相機位置進着色器:

// 傳遞光源位置
glUniform3fv(glGetUniformLocation(program, "lightPos"), 1, glm::value_ptr(lightPos));

// 傳遞相機位置
glUniform3fv(glGetUniformLocation(program, "cameraPos"), 1, glm::value_ptr(cameraPosition));
           

同時我們記得增加控制光源移動的接口:

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

最後我們用一個模型,将他始終綁定在光源的位置,以表示我們光源的位置。唔。。。就選最後一個模型罷:

重新加載程式:

OpenGL學習(八)phong光照模型前言Model 類的小小改動phong 光照模型在着色器中實作 phong 光照完整代碼

好耶!

完整代碼

c++

// std c++
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <map>
#include <sstream>
#include <iostream>

// glew glut
#include <GL/glew.h>
#include <GL/freeglut.h>

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

// SOIL
#include <SOIL2/SOIL2.h>

// assimp
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

// --------------------- end of include --------------------- //

class Mesh
{
public:
    // OpenGL 對象
    GLuint vao, vbo, ebo;
    GLuint diffuseTexture;  // 漫反射紋理

    // 頂點屬性
    std::vector<glm::vec3> vertexPosition;
    std::vector<glm::vec2> vertexTexcoord;
    std::vector<glm::vec3> vertexNormal;

    // glDrawElements 函數的繪制索引
    std::vector<int> index;

    Mesh() {}
    void bindData()
    {
        // 建立頂點數組對象
        glGenVertexArrays(1, &vao); // 配置設定1個頂點數組對象
        glBindVertexArray(vao);  	// 綁定頂點數組對象

        // 建立并初始化頂點緩存對象 這裡填NULL 先不傳資料
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER,
            vertexPosition.size() * sizeof(glm::vec3) +
            vertexTexcoord.size() * sizeof(glm::vec2) +
            vertexNormal.size() * sizeof(glm::vec3),
            NULL, GL_STATIC_DRAW);

        // 傳位置
        GLuint offset_position = 0;
        GLuint size_position = vertexPosition.size() * sizeof(glm::vec3);
        glBufferSubData(GL_ARRAY_BUFFER, offset_position, size_position, vertexPosition.data());
        glEnableVertexAttribArray(0);   // 着色器中 (layout = 0) 表示頂點位置
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (GLvoid*)(offset_position));
        // 傳紋理坐标
        GLuint offset_texcoord = size_position;
        GLuint size_texcoord = vertexTexcoord.size() * sizeof(glm::vec2);
        glBufferSubData(GL_ARRAY_BUFFER, offset_texcoord, size_texcoord, vertexTexcoord.data());
        glEnableVertexAttribArray(1);   // 着色器中 (layout = 1) 表示紋理坐标
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*)(offset_texcoord));
        // 傳法線
        GLuint offset_normal = size_position + size_texcoord;
        GLuint size_normal = vertexNormal.size() * sizeof(glm::vec3);
        glBufferSubData(GL_ARRAY_BUFFER, offset_normal, size_normal, vertexNormal.data());
        glEnableVertexAttribArray(2);   // 着色器中 (layout = 2) 表示法線
        glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, (GLvoid*)(offset_normal));

        // 傳索引到 ebo
        glGenBuffers(1, &ebo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, index.size() * sizeof(GLuint), index.data(), GL_STATIC_DRAW);

        glBindVertexArray(0);
    }
    void draw(GLuint program)
    {
        glBindVertexArray(vao);

        // 傳紋理
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, diffuseTexture);
        glUniform1i(glGetUniformLocation(program, "texture"), 0);

        // 繪制
        glDrawElements(GL_TRIANGLES, this->index.size(), GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);
    }
};

class Model
{
public:
    std::vector<Mesh> meshes;
    std::map<std::string, GLuint> textureMap;
    glm::vec3 translate=glm::vec3(0,0,0), rotate = glm::vec3(0, 0, 0), scale = glm::vec3(1, 1, 1);
    Model() {}
    void load(std::string filepath)
    {
        Assimp::Importer import;
        const aiScene* scene = import.ReadFile(filepath, aiProcess_Triangulate);
        // 異常處理
        if (!scene || scene->mFlags == AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
        {
            std::cout << "讀取模型出現錯誤: " << import.GetErrorString() << std::endl;
            exit(-1);
        }
        // 模型檔案相對路徑
        std::string rootPath = filepath.substr(0, filepath.find_last_of('/'));

        // 循環生成 mesh
        for (int i = 0; i < scene->mNumMeshes; i++)
        {
            // 引用目前mesh
            meshes.push_back(Mesh());
            Mesh& mesh = meshes.back();

            // 擷取 assimp 的讀取到的 aimesh 對象
            aiMesh* aimesh = scene->mMeshes[i];

            // 我們将資料傳遞給我們自定義的mesh
            for (int j = 0; j < aimesh->mNumVertices; j++)
            {
                // 頂點
                glm::vec3 vvv;
                vvv.x = aimesh->mVertices[j].x;
                vvv.y = aimesh->mVertices[j].y;
                vvv.z = aimesh->mVertices[j].z;
                mesh.vertexPosition.push_back(vvv);

                // 法線
                vvv.x = aimesh->mNormals[j].x;
                vvv.y = aimesh->mNormals[j].y;
                vvv.z = aimesh->mNormals[j].z;
                mesh.vertexNormal.push_back(vvv);

                // 紋理坐标: 如果存在則加入。assimp 預設可以有多個紋理坐标 我們取第一個(0)即可
                glm::vec2 vv(0, 0);
                if (aimesh->mTextureCoords[0])
                {
                    vv.x = aimesh->mTextureCoords[0][j].x;
                    vv.y = aimesh->mTextureCoords[0][j].y;
                }
                mesh.vertexTexcoord.push_back(vv);
            }

            // 如果有材質,那麼傳遞材質
            if (aimesh->mMaterialIndex >= 0)
            {
                // 擷取目前 aimesh 的材質對象
                aiMaterial* material = scene->mMaterials[aimesh->mMaterialIndex];

                // 擷取 diffuse 貼圖檔案路徑名稱 我們隻取1張貼圖 故填 0 即可
                aiString aistr;
                material->GetTexture(aiTextureType_DIFFUSE, 0, &aistr);
                std::string texpath = aistr.C_Str();
                texpath = rootPath + '/' + texpath;   // 取相對路徑

                // 如果沒生成過紋理,那麼生成它
                if (textureMap.find(texpath) == textureMap.end())
                {
                    // 生成紋理
                    GLuint tex;
                    glGenTextures(1, &tex);
                    glBindTexture(GL_TEXTURE_2D, tex);
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
                    int textureWidth, textureHeight;
                    unsigned char* image = SOIL_load_image(texpath.c_str(), &textureWidth, &textureHeight, 0, SOIL_LOAD_RGB);
                    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, textureWidth, textureHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);   // 生成紋理
                    delete[] image;

                    textureMap[texpath] = tex;
                }

                // 傳遞紋理
                mesh.diffuseTexture = textureMap[texpath];
            }

            // 傳遞面片索引
            for (GLuint j = 0; j < aimesh->mNumFaces; j++)
            {
                aiFace face = aimesh->mFaces[j];
                for (GLuint k = 0; k < face.mNumIndices; k++)
                {
                    mesh.index.push_back(face.mIndices[k]);
                }
            }

            mesh.bindData();
        }
    }
    void draw(GLuint program)
    {
        // 傳模型矩陣
        glm::mat4 unit(    // 機關矩陣
            glm::vec4(1, 0, 0, 0),
            glm::vec4(0, 1, 0, 0),
            glm::vec4(0, 0, 1, 0),
            glm::vec4(0, 0, 0, 1)
        );
        glm::mat4 scale = glm::scale(unit, this->scale);
        glm::mat4 translate = glm::translate(unit, this->translate);

        glm::mat4 rotate = unit;    // 旋轉
        rotate = glm::rotate(rotate, glm::radians(this->rotate.x), glm::vec3(1, 0, 0)); 
        rotate = glm::rotate(rotate, glm::radians(this->rotate.y), glm::vec3(0, 1, 0));  
        rotate = glm::rotate(rotate, glm::radians(this->rotate.z), glm::vec3(0, 0, 1));

        // 模型變換矩陣
        glm::mat4 model = translate * rotate * scale;
        GLuint mlocation = glGetUniformLocation(program, "model");    // 名為model的uniform變量的位置索引
        glUniformMatrix4fv(mlocation, 1, GL_FALSE, glm::value_ptr(model));   // 列優先矩陣

        for (int i = 0; i < meshes.size(); i++)
        {
            meshes[i].draw(program);
        }
    }
};

// ---------------------------- end of class definition ---------------------------- //

std::vector<Model> models;

GLuint program; // 着色器程式對象  

// 相機參數
glm::vec3 cameraPosition(0, 0, 0);      // 相機位置
glm::vec3 cameraDirection(0, 0, -1);    // 相機視線方向
glm::vec3 cameraUp(0, 1, 0);            // 世界空間下豎直向上向量
float pitch = 0.0f;
float roll = 0.0f;
float yaw = 0.0f;

// 光源
glm::vec3 lightPos = glm::vec3(0, 0.5, 0);

// 視界體參數
float left = -1, right = 1, bottom = -1, top = 1, zNear = 0.01, zFar = 100.0;

int windowWidth = 512;  // 視窗寬
int windowHeight = 512; // 視窗高

bool keyboardState[1024];   // 鍵盤狀态數組 keyboardState[x]==true 表示按下x鍵

// --------------- end of global variable definition --------------- //

// 讀取檔案并且傳回一個長字元串表示檔案内容
std::string readShaderFile(std::string filepath)
{
    std::string res, line;
    std::ifstream fin(filepath);
    if (!fin.is_open())
    {
        std::cout << "檔案 " << filepath << " 打開失敗" << std::endl;
        exit(-1);
    }
    while (std::getline(fin, line))
    {
        res += line + '\n';
    }
    fin.close();
    return res;
}

// 擷取着色器對象
GLuint getShaderProgram(std::string fshader, std::string vshader)
{
    // 讀取shader源檔案
    std::string vSource = readShaderFile(vshader);
    std::string fSource = readShaderFile(fshader);
    const char* vpointer = vSource.c_str();
    const char* fpointer = fSource.c_str();

    // 容錯
    GLint success;
    GLchar infoLog[512];

    // 建立并編譯頂點着色器
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, (const GLchar**)(&vpointer), NULL);
    glCompileShader(vertexShader);
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);   // 錯誤檢測
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "頂點着色器編譯錯誤\n" << infoLog << std::endl;
        exit(-1);
    }

    // 建立并且編譯片段着色器
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, (const GLchar**)(&fpointer), NULL);
    glCompileShader(fragmentShader);
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);   // 錯誤檢測
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "片段着色器編譯錯誤\n" << infoLog << std::endl;
        exit(-1);
    }

    // 連結兩個着色器到program對象
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    // 删除着色器對象
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return shaderProgram;
}

// 滑鼠滾輪函數
void mouseWheel(int wheel, int direction, int x, int y)
{
    // zFar += 1 * direction * 0.1;
    glutPostRedisplay();    // 重繪
}

// 滑鼠運動函數
void mouse(int x, int y)
{
    // 調整旋轉
    yaw += 35 * (x - float(windowWidth) / 2.0) / windowWidth;
    yaw = glm::mod(yaw + 180.0f, 360.0f) - 180.0f;    // 取模範圍 -180 ~ 180

    pitch += -35 * (y - float(windowHeight) / 2.0) / windowHeight;
    pitch = glm::clamp(pitch, -89.0f, 89.0f);

    glutWarpPointer(windowWidth / 2.0, windowHeight / 2.0);
    glutPostRedisplay();    // 重繪
}

// 鍵盤回調函數
void keyboardDown(unsigned char key, int x, int y)
{
    keyboardState[key] = true;
}
void keyboardDownSpecial(int key, int x, int y)
{
    keyboardState[key] = true;
}
void keyboardUp(unsigned char key, int x, int y)
{
    keyboardState[key] = false;
}
void keyboardUpSpecial(int key, int x, int y)
{
    keyboardState[key] = false;
}
// 根據鍵盤狀态判斷移動
void move()
{
    float cameraSpeed = 0.0015f;
    if (keyboardState['w']) cameraPosition += cameraSpeed * cameraDirection;
    if (keyboardState['s']) cameraPosition -= cameraSpeed * cameraDirection;
    if (keyboardState['a']) cameraPosition -= cameraSpeed * glm::normalize(glm::cross(cameraDirection, cameraUp));
    if (keyboardState['d']) cameraPosition += cameraSpeed * glm::normalize(glm::cross(cameraDirection, cameraUp));
    if (keyboardState[GLUT_KEY_CTRL_L]) cameraPosition.y -= cameraSpeed;
    if (keyboardState[' ']) cameraPosition.y += cameraSpeed;
    if (keyboardState['i']) lightPos.x += cameraSpeed;
    if (keyboardState['I']) lightPos.x -= cameraSpeed;
    if (keyboardState['o']) lightPos.y += cameraSpeed;
    if (keyboardState['O']) lightPos.y -= cameraSpeed;
    glutPostRedisplay();    // 重繪
}

// 初始化
void init()
{
    // 生成着色器程式對象
    std::string fshaderPath = "shaders/fshader.fsh";
    std::string vshaderPath = "shaders/vshader.vsh";
    program = getShaderProgram(fshaderPath, vshaderPath);
    glUseProgram(program);  // 使用着色器

    Model tree1 = Model();
    tree1.translate = glm::vec3(0.25, 0, -1);
    tree1.scale = glm::vec3(0.00025, 0.00025, 0.00025);
    tree1.load("models/tree/tree02.obj");
    models.push_back(tree1);
    
    Model tree2 = Model();
    tree2.translate = glm::vec3(1, 0, -1);
    tree2.scale = glm::vec3(0.00015, 0.00015, 0.00015);
    tree2.load("models/tree/tree02.obj");
    models.push_back(tree2);

    Model plane = Model();
    plane.translate = glm::vec3(0, -0.1, 0);
    plane.rotate = glm::vec3(0, -90, 0);
    plane.load("models/plane/plane.obj");
    models.push_back(plane);

    Model vlight = Model();
    vlight.translate = glm::vec3(1, 0, -1);
    vlight.scale = glm::vec3(0.15, 0.15, 0.15);
    vlight.load("models/Indoor plant 3/Low-Poly Plant_.obj");
    models.push_back(vlight);

    glEnable(GL_DEPTH_TEST);  // 開啟深度測試

    glClearColor(1.0, 1.0, 1.0, 1.0);   // 背景顔色
}

// 顯示回調函數
void display()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);       // 清空視窗顔色緩存

    move(); // 移動控制 -- 控制相機位置

    // 計算歐拉角以确定相機朝向
    cameraDirection.x = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
    cameraDirection.y = sin(glm::radians(pitch));
    cameraDirection.z = -cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 相機看向z軸負方向

    // 傳視圖矩陣
    glm::mat4 view = glm::lookAt(cameraPosition, cameraPosition + cameraDirection, cameraUp);
    GLuint vlocation = glGetUniformLocation(program, "view");
    glUniformMatrix4fv(vlocation, 1, GL_FALSE, glm::value_ptr(view));

    // 傳投影矩陣
    glm::mat4 projection = glm::perspective(glm::radians(70.0f), (GLfloat)windowWidth / (GLfloat)windowHeight, zNear, zFar);
    GLuint plocation = glGetUniformLocation(program, "projection");
    glUniformMatrix4fv(plocation, 1, GL_FALSE, glm::value_ptr(projection));

    // 傳遞光源位置
    glUniform3fv(glGetUniformLocation(program, "lightPos"), 1, glm::value_ptr(lightPos));

    // 傳遞相機位置
    glUniform3fv(glGetUniformLocation(program, "cameraPos"), 1, glm::value_ptr(cameraPosition));

    models.back().translate = lightPos;

    for (auto m : models)
    {
        m.draw(program);
    }

    glutSwapBuffers();                  // 交換緩沖區
}

// -------------------------------- main -------------------------------- //

int main(int argc, char** argv)
{
    glutInit(&argc, argv);              // glut初始化
    glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH);
    glutInitWindowSize(windowWidth, windowHeight);// 視窗大小
    glutCreateWindow("7 - phong"); // 建立OpenGL上下文

#ifdef __APPLE__
#else
    glewInit();
#endif

    init();

    // 綁定滑鼠移動函數 -- 
    //glutMotionFunc(mouse);  // 左鍵按下并且移動
    glutPassiveMotionFunc(mouse);   // 滑鼠直接移動
    //glutMouseWheelFunc(mouseWheel); // 滾輪縮放

    // 綁定鍵盤函數
    glutKeyboardFunc(keyboardDown);
    glutSpecialFunc(keyboardDownSpecial);
    glutKeyboardUpFunc(keyboardUp);
    glutSpecialUpFunc(keyboardUpSpecial);

    glutDisplayFunc(display);           // 設定顯示回調函數 -- 每幀執行
    glutMainLoop();                     // 進入主循環

    return 0;
}
           

着色器

頂點:

#version 330 core

// 頂點着色器輸入
layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec2 vTexcoord;
layout (location = 2) in vec3 vNormal;

out vec3 worldPos;
out vec2 texcoord;
out vec3 normal;

uniform mat4 model;         // 模型變換矩陣
uniform mat4 view;          // 模型變換矩陣
uniform mat4 projection;    // 模型變換矩陣

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

    // 傳遞到片段着色器
    texcoord = vTexcoord;   
    worldPos = (model * vec4(vPosition, 1.0)).xyz;
    normal = (model * vec4(vNormal, 0.0)).xyz;
}
           

片元:

#version 330 core

in vec3 worldPos;   // 目前片元的世界坐标
in vec2 texcoord;   // 紋理坐标
in vec3 normal;     // 法向量

out vec4 fColor;    // 片元輸出像素的顔色

uniform sampler2D texture;  // 紋理圖檔

uniform vec3 lightPos;  // 光源位置
uniform vec3 cameraPos; // 相機位置

float phong(vec3 worldPos, vec3 cameraPos, vec3 lightPos, vec3 normal)
{
    vec3 N = normalize(normal);
    vec3 V = normalize(worldPos - cameraPos);
    vec3 L = normalize(worldPos - lightPos);
    vec3 R = reflect(L, N);

    float ambient = 0.3;
    float diffuse = max(dot(N, -L), 0) * 0.7;
    float specular = pow(max(dot(-R, V), 0), 50.0) * 1.1;

    return ambient + diffuse + specular;
}

void main()
{
    fColor.rgb =  texture2D(texture, texcoord).rgb;
    fColor.rgb *= phong(worldPos, cameraPos, lightPos, normal);
}
           

繼續閱讀