OpenGL高階版本學習日誌2:光照模型&材質

程式猿老甘發表於2020-12-07

 

1. 前言

促使我學習OpenGL新版本的一大動力,就是對改善渲染質量的渴望。談到渲染,就不能不提光照模型。好的光照模型,對物體的真實感渲染起到至關重要的作用。本章我將從光照模型這個主題切入,來談一談如何通過編寫shader來控制光照。

注:本章的部分內容可能存在主管成分,未經嚴格的認證,如有問題,請以OpenGL新版教材為準。關於新版本的一些基礎知識,如VBO,VAO的繫結,管線程式設計機制等,在本文中不做具體介紹。

2. 光照模型

作為一個經典的光照模型,馮氏光照模型(Phong Lighting Model)被廣泛使用,其主要結構包含三個部分。環境光照,漫反射光照和鏡面光照。

環境光照通常在沒有特殊光源的情況下,我們在白天仍然能夠看清室內室外的物體,其原因就是因為存在環境光。環境光主要指的就是太陽光。太陽光在通過多次反射與衍射,形成了一個近似均勻的光場。基於這樣一個光場,物體才能夠被看到。在沒有太陽光照的晚上,沒有特殊光源的輔助,自然就看不到物體了。在圖形渲染引擎中,為了能夠看到物體,通常會預設加入這樣一個環境光作為基礎,以模擬太陽光產生的均勻光場。

漫反射光照:在環境光的基礎上,如果有特殊的光源,自然會對物體的光照渲染有不同的影響。比如當有一盞檯燈的地方,靠近檯燈,且面向檯燈的一面,自然會接收到更強的光照,在渲染時產生更加明亮的效果。由特別光源驅動,並更具物體的不同位置與接受光源的不同角度產生的光照效果,即為漫反射光照。通常意義上,漫反射光照是所有光照最明顯,對物體光照渲染影響最大的一個。

鏡面光照:俗稱高光。當我們的觀察視角和光源反射到觀察視角的角度約接近,越能夠看到物體的反射區域呈現出一塊比其他區域明亮的地方,這個明亮的區域就是高光,即鏡面光照。如果你對素描有一定的基礎,則對高光的理解會更加深入。高光通常反映了觀察視角和渲染物體本身反射光照的位置關係以及渲染物體表面本身的材質。

將以上三種光照疊加,就組成了馮氏光照模型。

3. 利用Shader實現光照模型

這裡需要分為兩個部分。一個部分是光源的,一個部分是對物體的。

光源比較好理解,這裡就是一個點光源,其顏色與亮度是恆定的。它的頂點著色器為:

#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);
}

就是一個最基本的檢視變換。其片段著色器同樣簡單,就是一個單一顏色的光源。

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0); // set alle 4 vector values to 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()
{
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;  
    
    gl_Position = projection * view * vec4(FragPos, 1.0);
}

除了基本的檢視座標變換以外,我們發現還有兩個屬性,即FragPos和Normal,對應世界座標和法向。這兩個屬性對於光照渲染十分重要,因此需要在頂點座標變換的時候進行輸出。世界座標定義了物體與光源的距離與入射角度,法向定義了物體與光源的角度。有這兩個屬性,就能夠決定在片段著色器中如何實現光照渲染。以下就是光照渲染中最核心的物體片段著色器程式碼:

#version 330 core
out vec4 FragColor;

in vec3 Normal;  
in vec3 FragPos;  
  
uniform vec3 lightPos; 
uniform vec3 viewPos; 
uniform vec3 lightColor;
uniform vec3 objectColor;

void main()
{
    // ambient
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;
  	
    // diffuse 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // specular
    float specularStrength = 0.5;
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 64);
    vec3 specular = specularStrength * spec * lightColor;  
        
    vec3 result = (ambient + diffuse + specular) * objectColor;
    FragColor = vec4(result, 1.0);
} 

由於這段程式碼對於理解光照渲染十分重要,我們逐句進行解釋:

1)float ambientStrength = 0.1; 
      vec3 ambient = ambientStrength * lightColor;

最容易理解的就是環境光。輸入基礎光照和定義一個基礎強度,就定義了環境光。

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

首先單位化法向,方便計算。之後計算入射光線法向,同時也進行單位化。注意,在計算入射光線角度的時候,用到了世界座標,這就是為什麼需要世界座標的原因了。接下來就是根據物體一點的法向與入射光線角度的點積,進而得到漫反射強度。當兩個向量       的夾角為90度時,漫反射強度為0,可以想象成一個二維面片,用它的邊正對光源,使之面向光源的面積無限趨近於0,即沒有任何反射。當超過90度時,即將被光面面向光源,為了防止渲染錯誤,顧設定最小值為0。

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

鏡面反射主要涉及到觀察角度與光源反射角度的關係。首先獲得觀察方向,即攝像機與物體位置生成的向量viewDir。反射角度也好理解,就是以法線為準,以入射光線為輸入,輸出一個反射光線的向量R,計算R與方向的角度Θ,即內積。使用pow以及次數來控制反射強度,因為max(dot(viewDir, reflectDir), 0.0)最大值為1,所以使用的次數越大,相當於強度約弱。結合基礎強度加夾角強度以及光照顏色,這樣就得到了鏡面光照的結果。

   

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

最後將三個光照加在一起,併疊加上物體本身的顏色,得到馮氏光照模型的最後結果。

完整程式碼連結:https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.2.basic_lighting_specular/basic_lighting_specular.cpp

4. 材質

如果你瞭解了整個的光照模型,你會發現可以通過一些引數來改變光照的效果,如法線,光照強度,反射強度等等。這些引數在現實中反映了物體表面的一些物理性質。我們將這些資料進行打包,就能夠得到具有特定物理性質的渲染結果。這種對物理性質模擬的打包,可以被成為材質,如下所示:

#version 330 core
struct Material {
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float shininess;
}; 

uniform Material material;

可以看到shader用一個結構體來打包了光照模型的基本資料,片段著色器也就被改為:

void main()
{    
    // 環境光
    vec3 ambient = lightColor * material.ambient;

    // 漫反射 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = lightColor * (diff * material.diffuse);

    // 鏡面光
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    vec3 specular = lightColor * (spec * material.specular);  

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

可以看到,材質的引數在各個光照分量中都施加偶爾自己的影響。通過對這些引數進行特定的組合,能夠得到對不同材質的光照模擬:

相關文章