OpenGL 圖形庫的使用(四十三)—— PBR之光照Lighting

版本記錄

版本號(hào) 時(shí)間
V1.0 2018.01.20

前言

OpenGL 圖形庫項(xiàng)目中一直也沒用過,最近也想學(xué)著使用這個(gè)圖形庫,感覺還是很有意思,也就自然想著好好的總結(jié)一下,希望對(duì)大家能有所幫助。下面內(nèi)容來自歡迎來到OpenGL的世界。
1. OpenGL 圖形庫使用(一) —— 概念基礎(chǔ)
2. OpenGL 圖形庫使用(二) —— 渲染模式、對(duì)象、擴(kuò)展和狀態(tài)機(jī)
3. OpenGL 圖形庫使用(三) —— 著色器、數(shù)據(jù)類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標(biāo)系統(tǒng)之五種不同的坐標(biāo)系統(tǒng)(一)
8. OpenGL 圖形庫的使用(八)—— 坐標(biāo)系統(tǒng)之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(jī)(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(jī)(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫的使用(十二)—— 光照之基礎(chǔ)光照
13. OpenGL 圖形庫的使用(十三)—— 光照之材質(zhì)
14. OpenGL 圖形庫的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫的使用(十七)—— 光照之復(fù)習(xí)總結(jié)
18. OpenGL 圖形庫的使用(十八)—— 模型加載之Assimp
19. OpenGL 圖形庫的使用(十九)—— 模型加載之網(wǎng)格
20. OpenGL 圖形庫的使用(二十)—— 模型加載之模型
21. OpenGL 圖形庫的使用(二十一)—— 高級(jí)OpenGL之深度測(cè)試
22. OpenGL 圖形庫的使用(二十二)—— 高級(jí)OpenGL之模板測(cè)試Stencil testing
23. OpenGL 圖形庫的使用(二十三)—— 高級(jí)OpenGL之混合Blending
24. OpenGL 圖形庫的使用(二十四)—— 高級(jí)OpenGL之面剔除Face culling
25. OpenGL 圖形庫的使用(二十五)—— 高級(jí)OpenGL之幀緩沖Framebuffers
26. OpenGL 圖形庫的使用(二十六)—— 高級(jí)OpenGL之立方體貼圖Cubemaps
27. OpenGL 圖形庫的使用(二十七)—— 高級(jí)OpenGL之高級(jí)數(shù)據(jù)Advanced Data
28. OpenGL 圖形庫的使用(二十八)—— 高級(jí)OpenGL之高級(jí)GLSL Advanced GLSL
29. OpenGL 圖形庫的使用(二十九)—— 高級(jí)OpenGL之幾何著色器Geometry Shader
30. OpenGL 圖形庫的使用(三十)—— 高級(jí)OpenGL之實(shí)例化Instancing
31. OpenGL 圖形庫的使用(三十一)—— 高級(jí)OpenGL之抗鋸齒Anti Aliasing
32. OpenGL 圖形庫的使用(三十二)—— 高級(jí)光照之高級(jí)光照Advanced Lighting
33. OpenGL 圖形庫的使用(三十三)—— 高級(jí)光照之Gamma校正Gamma Correction
34. OpenGL 圖形庫的使用(三十四)—— 高級(jí)光照之陰影 - 陰影映射Shadow Mapping
35. OpenGL 圖形庫的使用(三十五)—— 高級(jí)光照之陰影 - 點(diǎn)陰影Point Shadows
36. OpenGL 圖形庫的使用(三十六)—— 高級(jí)光照之法線貼圖Normal Mapping
37. OpenGL 圖形庫的使用(三十七)—— 高級(jí)光照之視差貼圖Parallax Mapping
38. OpenGL 圖形庫的使用(三十八)—— 高級(jí)光照之HDR
39. OpenGL 圖形庫的使用(三十九)—— 高級(jí)光照之泛光
40. OpenGL 圖形庫的使用(四十)—— 高級(jí)光照之延遲著色法Deferred Shading
41. OpenGL 圖形庫的使用(四十一)—— 高級(jí)光照之SSAO
42. OpenGL 圖形庫的使用(四十二)—— PBR之理論Theory

光照

注意: 作者正在對(duì)PBR章節(jié)進(jìn)行大的調(diào)整,原文的內(nèi)容時(shí)時(shí)可能有更新,建議仍是閱讀原文。

譯者注:
閱讀本節(jié)請(qǐng)熟悉上一節(jié)提到的幾個(gè)名詞:

  • 輻射通量(Radiant flux)
  • 輻射率(Radiance)
  • 輻照度(Irradiance)
  • 輻射強(qiáng)度(Radiant Intensity)

上一個(gè)教程中,我們討論了一些PBR渲染的基礎(chǔ)知識(shí)。 在本章節(jié)中,我們將重點(diǎn)放在把以前討論過的理論轉(zhuǎn)化為實(shí)際的渲染器,這個(gè)渲染器將使用直接的(或解析的)光源:比如點(diǎn)光源,定向燈或聚光燈。

我們先來看看上一個(gè)章提到的反射方程的最終版:

我們大致上清楚這個(gè)反射方程在干什么,但我們?nèi)匀涣粲幸恍┟造F尚未揭開。比如說我們究竟將怎樣表示場(chǎng)景上的輻照度(Irradiance), 輻射率(Radiance) L? 我們知道輻射率L(在計(jì)算機(jī)圖形領(lǐng)域中)表示在給定立體角ω的情況下光源的輻射通量(Radiant flux)?或光源在角度ω下發(fā)送出來的光能。 在我們的情況下,不妨假設(shè)立體角ω?zé)o限小,這樣輻射度就表示光源在一條光線或單個(gè)方向向量上的輻射通量。

基于以上的知識(shí),我們?nèi)绾螌⑵滢D(zhuǎn)化為以前的教程中積累的一些光照知識(shí)呢? 那么想象一下,我們有一個(gè)點(diǎn)光源(一個(gè)光源在所有方向具有相同的亮度),它的輻射通量為用RBG表示為(23.47,21.31,20.79)。該光源的輻射強(qiáng)度(Radiant Intensity)等于其在所有出射光線的輻射通量。 然而,當(dāng)我們?yōu)橐粋€(gè)表面上的特定的點(diǎn)p著色時(shí),在其半球領(lǐng)域Ω的所有可能的入射方向上,只有一個(gè)入射方向向量ωi直接來自于該點(diǎn)光源。 假設(shè)我們?cè)趫?chǎng)景中只有一個(gè)光源,位于空間中的某一個(gè)點(diǎn),因而對(duì)于p點(diǎn)的其他可能的入射光線方向上的輻射率為0:

如果從一開始,我們就假設(shè)點(diǎn)光源不受光線衰減(光照強(qiáng)度會(huì)隨著距離變暗)的影響,那么無論我們把光源放在哪,入射光線的輻射率總是一樣的(除去入射角cosθ對(duì)輻射率的影響之外)。 正是因?yàn)闊o論我們從哪個(gè)角度觀察它,點(diǎn)光源總具有相同的輻射強(qiáng)度,我們可以有效地將其輻射強(qiáng)度建模為其輻射通量: 一個(gè)常量向量(23.47,21.31,20.79)

然而,輻射率也需要將位置p作為輸入,正如所有現(xiàn)實(shí)的點(diǎn)光源都會(huì)受光線衰減影響一樣,點(diǎn)光源的輻射強(qiáng)度應(yīng)該根據(jù)點(diǎn)p所在的位置和光源的位置以及他們之間的距離而做一些縮放。 因此,根據(jù)原始的輻射方程,我們會(huì)根據(jù)表面法向量n和入射角度wi來縮放光源的輻射強(qiáng)度。

在實(shí)現(xiàn)上來說:對(duì)于直接點(diǎn)光源的情況,輻射率函數(shù)LL先獲取光源的顏色值, 然后光源和某點(diǎn)p的距離衰減,接著按照n?wi縮放,但是僅僅有一條入射角為wi的光線打在點(diǎn)p上, 這個(gè)wi同時(shí)也等于在p點(diǎn)光源的方向向量。寫成代碼的話會(huì)是這樣:

vec3  lightColor  = vec3(23.47, 21.31, 20.79);
vec3  wi          = normalize(lightPos - fragPos);
float cosTheta    = max(dot(N, Wi), 0.0);
float attenuation = calculateAttenuation(fragPos, lightPos);
float radiance    = lightColor * attenuation * cosTheta;

除了一些叫法上的差異以外,這段代碼對(duì)你們來說應(yīng)該很TM熟悉:這正是我們一直以來怎么計(jì)算(漫反射(diffuse))光照的!當(dāng)涉及到直接照明(direct lighting)時(shí),輻射率的計(jì)算方式和我們之前計(jì)算當(dāng)只有一個(gè)光源照射在物體表面的時(shí)候非常相似。

請(qǐng)注意,這個(gè)假設(shè)是成立的條件是點(diǎn)光源體積無限小,相當(dāng)于在空間中的一個(gè)點(diǎn)。如果我們認(rèn)為該光源是具有體積的,它的輻射會(huì)在一個(gè)以上的入射光的方向不等于零。

對(duì)于其它類型的從單點(diǎn)發(fā)出來的光源我們類似地計(jì)算出輻射率。比如,定向光(directional light)擁有恒定的wi而不會(huì)有衰減因子;而一個(gè)聚光燈光源則沒有恒定的輻射強(qiáng)度,其輻射強(qiáng)度是根據(jù)聚光燈的方向向量來縮放的。

這也讓我們回到了對(duì)于表面的半球領(lǐng)域(hemisphere)Ω的積分∫上。由于我們事先知道的所有貢獻(xiàn)光源的位置,因此對(duì)物體表面上的一個(gè)點(diǎn)著色并不需要我們嘗試去求解積分。我們可以直接拿光源的(已知的)數(shù)目,去計(jì)算它們的總輻照度,因?yàn)槊總€(gè)光源僅僅只有一個(gè)方向上的光線會(huì)影響物體表面的輻射率。這使得PBR對(duì)直接光源的計(jì)算相對(duì)簡(jiǎn)單,因?yàn)槲覀冎恍枰行У乇闅v所有有貢獻(xiàn)的光源。而當(dāng)我們后來把環(huán)境照明也考慮在內(nèi)的IBL教程中,我們就必須采取積分去計(jì)算了,這是因?yàn)楣饩€可能會(huì)在任何一個(gè)方向入射。


一個(gè)PBR表面模型

現(xiàn)在讓我們開始寫片段著色器來實(shí)現(xiàn)上述的PBR模型吧~ 首先我們需要把PBR相關(guān)的輸入放進(jìn)片段著色器。

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;

uniform vec3 camPos;

uniform vec3  albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;

我們把通用的頂點(diǎn)著色器的輸出作為輸入的一部分。另一部分輸入則是物體表面模型的一些材質(zhì)參數(shù)。

然后再片段著色器的開始部分我們做一下任何光照算法都需要做的計(jì)算:

void main()
{
    vec3 N = normalize(Normal); 
    vec3 V = normalize(camPos - WorldPos);
    [...]
}

直接光照明

在本教程的例子中我們會(huì)采用總共4個(gè)點(diǎn)光源來直接表示場(chǎng)景的輻照度。為了滿足反射率方程,我們循環(huán)遍歷每一個(gè)光源,計(jì)算他們獨(dú)立的輻射率然后求和,接著根據(jù)BRDF和光源的入射角來縮放該輻射率。我們可以把循環(huán)當(dāng)作在對(duì)物體的半球領(lǐng)域?qū)λ灾苯庸庠辞蠓e分。首先我們來計(jì)算一些可以預(yù)計(jì)算的光照變量:

vec3 Lo = vec3(0.0);
for(int i = 0; i < 4; ++i) 
{
    vec3 L = normalize(lightPositions[i] - WorldPos);
    vec3 H = normalize(V + L);

    float distance    = length(lightPositions[i] - WorldPos);
    float attenuation = 1.0 / (distance * distance);
    vec3 radiance     = lightColors[i] * attenuation; 
    [...]  

由于我們線性空間內(nèi)計(jì)算光照(我們會(huì)在著色器的尾部進(jìn)行Gamma校正),我們使用在物理上更為準(zhǔn)確的平方倒數(shù)作為衰減。

相對(duì)于物理上正確來說,你可能仍然想使用常量,線性或者二次衰減方程(他們?cè)谖锢砩舷鄬?duì)不準(zhǔn)確),卻可以為您提供在光的能量衰減更多的控制。

然后,對(duì)于每一個(gè)光源我們都想計(jì)算完整的Cook-Torrance specular BRDF項(xiàng):

首先我們想計(jì)算的是鏡面反射和漫反射的系數(shù), 或者說發(fā)生表面反射和折射的光線的比值。 我們從上一個(gè)教程知道可以使用菲涅爾方程計(jì)算:

vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}  

菲涅爾方程返回的是一個(gè)物體表面光線被反射的百分比, 也就是我們反射方程中的參數(shù)ks。Fresnel-Schlick近似接受一個(gè)參數(shù)F0,被稱為0°入射角的反射(surface reflection at zero incidence)表示如果直接(垂直)觀察表面的時(shí)候有多少光線會(huì)被反射。 這個(gè)參數(shù)F0會(huì)因?yàn)椴牧喜煌煌?,而且?huì)因?yàn)椴馁|(zhì)是金屬而發(fā)生變色。在PBR金屬流中我們簡(jiǎn)單地認(rèn)為大多數(shù)的絕緣體在F0為0.04的時(shí)候看起來視覺上是正確的,我們同時(shí)會(huì)特別指定F0當(dāng)我們遇到金屬表面并且給定反射率的時(shí)候。 因此代碼上看起來會(huì)像是這樣:

vec3 F0 = vec3(0.04); 
F0      = mix(F0, albedo, metallic);
vec3 F  = fresnelSchlick(max(dot(H, V), 0.0), F0);

你可以看到,對(duì)于非金屬材質(zhì)來說F0永遠(yuǎn)保持0.04這個(gè)值,我們會(huì)根據(jù)表面的金屬性來改變F0這個(gè)值, 并且在原來的F0和反射率中插值計(jì)算F0。

我們已經(jīng)算出F, 剩下的項(xiàng)就是計(jì)算正態(tài)分布函數(shù)D和幾何遮蔽函數(shù)G了。

因此一個(gè)直接PBR光照著色器中D和G的計(jì)算代碼類似于:

float DistributionGGX(vec3 N, vec3 H, float roughness)
{
    float a      = roughness*roughness;
    float a2     = a*a;
    float NdotH  = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;

    float nom   = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;

    return nom / denom;
}

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;

    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / denom;
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2  = GeometrySchlickGGX(NdotV, roughness);
    float ggx1  = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}

這里比較重要的是和上一個(gè)教程不同的是,我們直接傳了粗糙度(roughness)參數(shù)給上述的函數(shù);通過這種方式,我們可以針對(duì)每一個(gè)不同的項(xiàng)對(duì)粗糙度做一些修改。根據(jù)迪士尼公司給出的觀察以及后來被Epic Games公司采用的光照模型,光照在幾何遮蔽函數(shù)和正太分布函數(shù)中采用粗糙度的平方會(huì)讓光照看起來更加自然。

現(xiàn)在兩個(gè)函數(shù)都給出了定義,在計(jì)算反射的循環(huán)中計(jì)算NDF和G項(xiàng)變得非常自然:

float NDF = DistributionGGX(N, H, roughness);       
float G   = GeometrySmith(N, V, L, roughness);       

這樣我們就湊夠了足夠的項(xiàng)來計(jì)算Cook-Torrance BRDF:

vec3 nominator    = NDF * G * F;
float denominator = 4 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; 
vec3 specular     = nominator / denominator;  

注意我們?cè)诜帜疙?xiàng)中加了一個(gè)0.001為了避免出現(xiàn)除零錯(cuò)誤。

現(xiàn)在我們終于可以計(jì)算每個(gè)光源在反射率方程中的貢獻(xiàn)值了!因?yàn)榉颇鶢柗匠讨苯咏o出了kS, 我們可以使用F表示鏡面反射在所有打在物體表面上的光線的貢獻(xiàn)。 從kS我們很容易計(jì)算折射的比值kD

vec3 kS = F;
vec3 kD = vec3(1.0) - kS;

kD *= 1.0 - metallic;   

我們可以看作kS表示光能中被反射的能量的比例, 而剩下的光能會(huì)被折射, 比值即為kD。更進(jìn)一步來說,因?yàn)榻饘俨粫?huì)折射光線,因此不會(huì)有漫反射。所以如果表面是金屬的,我們會(huì)把系數(shù)kD變?yōu)?。 這樣,我們終于集齊所有變量來計(jì)算我們出射光線的值:

    const float PI = 3.14159265359;

    float NdotL = max(dot(N, L), 0.0);        
    Lo += (kD * albedo / PI + specular) * radiance * NdotL;
}

最終的結(jié)果Lo,或者說是出射光線的輻射率,實(shí)際上是反射率方程的在半球領(lǐng)域Ω的積分的結(jié)果。但是我們實(shí)際上不需要去求積,因?yàn)閷?duì)于所有可能的入射光線方向我們知道只有4個(gè)方向的入射光線會(huì)影響片段(像素)的著色。因?yàn)檫@樣,我們可以直接循環(huán)N次計(jì)算這些入射光線的方向(N也就是場(chǎng)景中光源的數(shù)目)。

比較重要的是我們沒有把kS乘進(jìn)去我們的反射率方程中,這是因?yàn)槲覀円呀?jīng)在specualr BRDF中乘了菲涅爾系數(shù)F了,因?yàn)閗S等于F,因此我們不需要再乘一次。

剩下的工作就是加一個(gè)環(huán)境光照項(xiàng)給Lo,然后我們就擁有了片段的最后顏色:

vec3 ambient = vec3(0.03) * albedo * ao;
vec3 color   = ambient + Lo;  

線性空間和HDR渲染

直到現(xiàn)在,我們假設(shè)的所有計(jì)算都在線性的顏色空間中進(jìn)行的,因此我們需要在著色器最后做伽馬矯正。 在線性空間中計(jì)算光照是非常重要的,因?yàn)镻BR要求所有輸入都是線性的,如果不是這樣,我們就會(huì)得到不正常的光照。另外,我們希望所有光照的輸入都盡可能的接近他們?cè)谖锢砩系娜≈?,這樣他們的反射率或者說顏色值就會(huì)在色譜上有比較大的變化空間。Lo作為結(jié)果可能會(huì)變大得很快(超過1),但是因?yàn)槟J(rèn)的LDR輸入而取值被截?cái)?。所以在伽馬矯正之前我們采用色調(diào)映射使Lo從LDR的值映射為HDR的值。

color = color / (color + vec3(1.0));
color = pow(color, vec3(1.0/2.2)); 

這里我們采用的色調(diào)映射方法為Reinhard 操作,使得我們?cè)谫ゑR矯正后可以保留盡可能多的輻照度變化。 我們沒有使用一個(gè)獨(dú)立的幀緩沖或者采用后期處理,所以我們需要直接在每一步光照計(jì)算后采用色調(diào)映射和伽馬矯正。

采用線性顏色空間和HDR在PBR渲染管線中非常重要。如果沒有這些操作,幾乎是不可能正確地捕獲到因光照強(qiáng)度變化的細(xì)節(jié),這最終會(huì)導(dǎo)致你的計(jì)算變得不正確,在視覺上看上去非常不自然。


完整的直接光照PBR著色器

現(xiàn)在剩下的事情就是把做好色調(diào)映射和伽馬矯正的顏色值傳給片段著色器的輸出,然后我們就擁有了自己的直接光照PBR著色器。 為了完整性,這里給出了完整的代碼:

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
in vec3 WorldPos;
in vec3 Normal;

// material parameters
uniform vec3  albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;

// lights
uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];

uniform vec3 camPos;

const float PI = 3.14159265359;

float DistributionGGX(vec3 N, vec3 H, float roughness);
float GeometrySchlickGGX(float NdotV, float roughness);
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness);
vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness);

void main()
{       
    vec3 N = normalize(Normal);
    vec3 V = normalize(camPos - WorldPos);

    vec3 F0 = vec3(0.04); 
    F0 = mix(F0, albedo, metallic);

    // reflectance equation
    vec3 Lo = vec3(0.0);
    for(int i = 0; i < 4; ++i) 
    {
        // calculate per-light radiance
        vec3 L = normalize(lightPositions[i] - WorldPos);
        vec3 H = normalize(V + L);
        float distance    = length(lightPositions[i] - WorldPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance     = lightColors[i] * attenuation;        

        // cook-torrance brdf
        float NDF = DistributionGGX(N, H, roughness);        
        float G   = GeometrySmith(N, V, L, roughness);      
        vec3 F    = fresnelSchlick(max(dot(H, V), 0.0), F0);       

        vec3 kS = F;
        vec3 kD = vec3(1.0) - kS;
        kD *= 1.0 - metallic;     

        vec3 nominator    = NDF * G * F;
        float denominator = 4 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; 
        vec3 specular     = nominator / denominator;

        // add to outgoing radiance Lo
        float NdotL = max(dot(N, L), 0.0);                
        Lo += (kD * albedo / PI + specular) * radiance * NdotL; 
    }   

    vec3 ambient = vec3(0.03) * albedo * ao;
    vec3 color = ambient + Lo;

    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2));  

    FragColor = vec4(color, 1.0);
}  

希望經(jīng)過上一個(gè)教程的理論知識(shí)以及學(xué)習(xí)過關(guān)于渲染方程的一些知識(shí)后,這個(gè)著色器看起來不會(huì)太可怕。如果我們采用這個(gè)著色器,加上4個(gè)點(diǎn)光源和一些球體,同時(shí)我們令這些球體的金屬性(metallic)和粗糙度(roughness)沿垂直方向和水平方向分別變化,我們會(huì)得到這樣的結(jié)果:

(上述圖片)從下往上球體的金屬性從0.0變到1.0, 從左到右球體的粗糙度從0.0變到1.0。你可以看到僅僅改變這兩個(gè)值,顯示的效果會(huì)發(fā)生巨大的改變!

你可以在這里找到整個(gè)demo的完整代碼。

#include <glad/glad.h>  
#include <GLFW/glfw3.h>  
#include <stb_image.h>  
#include <glm/glm.hpp>  
#include <glm/gtc/matrix_transform.hpp>  
#include <glm/gtc/type_ptr.hpp>  
#include <[learnopengl/shader.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader.h)>  
#include <[learnopengl/camera.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/camera.h)>  
#include <[learnopengl/model.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/model.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);
unsigned int loadTexture(const char *path);
void renderSphere();

// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = 800.0f / 2.0;
float lastY = 600.0 / 2.0;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_SAMPLES, 4);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    glfwMakeContextCurrent(window);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);

    // build and compile shaders
    // -------------------------
Shader shader("[1.1.pbr.vs](https://learnopengl.com/code_viewer_gh.php?code=src/6.pbr/1.1.lighting/1.1.pbr.vs)", "[1.1.pbr.fs](https://learnopengl.com/code_viewer_gh.php?code=src/6.pbr/1.1.lighting/1.1.pbr.fs)");

  shader.use();
    shader.setVec3("albedo", 0.5f, 0.0f, 0.0f);
    shader.setFloat("ao", 1.0f);

    // lights
    // ------
    glm::vec3 lightPositions[] = {
        glm::vec3(-10.0f,  10.0f, 10.0f),
        glm::vec3( 10.0f,  10.0f, 10.0f),
        glm::vec3(-10.0f, -10.0f, 10.0f),
        glm::vec3( 10.0f, -10.0f, 10.0f),
    };
    glm::vec3 lightColors[] = {
        glm::vec3(300.0f, 300.0f, 300.0f),
        glm::vec3(300.0f, 300.0f, 300.0f),
        glm::vec3(300.0f, 300.0f, 300.0f),
        glm::vec3(300.0f, 300.0f, 300.0f)
    };
    int nrRows    = 7;
    int nrColumns = 7;
    float spacing = 2.5;

    // initialize static shader uniforms before rendering
    // --------------------------------------------------
    glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    shader.use();
    shader.setMat4("projection", projection);

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

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

        // render
        // ------
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        shader.use();
        glm::mat4 view = camera.GetViewMatrix();
        shader.setMat4("view", view);
        shader.setVec3("camPos", camera.Position);

        // render rows*column number of spheres with varying metallic/roughness values scaled by rows and columns respectively
        glm::mat4 model;
        for (unsigned int row = 0; row < nrRows; ++row) 
        {
            shader.setFloat("metallic", (float)row / (float)nrRows);
            for (unsigned int col = 0; col < nrColumns; ++col) 
            {
                // we clamp the roughness to 0.025 - 1.0 as perfectly smooth surfaces (roughness of 0.0) tend to look a bit off
                // on direct lighting.
                shader.setFloat("roughness", glm::clamp((float)col / (float)nrColumns, 0.05f, 1.0f));
                
                model = glm::mat4();
                model = glm::translate(model, glm::vec3(
                    (float)(col - (nrColumns / 2)) * spacing, 
                    (float)(row - (nrRows / 2)) * spacing, 
                    0.0f
                ));
                shader.setMat4("model", model);
                renderSphere();
            }
        }

        // render light source (simply re-render sphere at light positions)
        // this looks a bit off as we use the same shader, but it'll make their positions obvious and 
        // keeps the codeprint small.
        for (unsigned int i = 0; i < sizeof(lightPositions) / sizeof(lightPositions[0]); ++i)
        {
            glm::vec3 newPos = lightPositions[i] + glm::vec3(sin(glfwGetTime() * 5.0) * 5.0, 0.0, 0.0);
            newPos = lightPositions[i];
            shader.setVec3("lightPositions[" + std::to_string(i) + "]", newPos);
            shader.setVec3("lightColors[" + std::to_string(i) + "]", lightColors[i]);

            model = glm::mat4();
            model = glm::translate(model, newPos);
            model = glm::scale(model, glm::vec3(0.5f));
            shader.setMat4("model", model);
            renderSphere();
        }

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 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;
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.ProcessKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.ProcessKeyboard(BACKWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.ProcessKeyboard(LEFT, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.ProcessKeyboard(RIGHT, deltaTime);
}

// 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; // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(yoffset);
}

// renders (and builds at first invocation) a sphere
// -------------------------------------------------
unsigned int sphereVAO = 0;
unsigned int indexCount;
void renderSphere()
{
    if (sphereVAO == 0)
    {
        glGenVertexArrays(1, &sphereVAO);

        unsigned int vbo, ebo;
        glGenBuffers(1, &vbo);
        glGenBuffers(1, &ebo);

        std::vector<glm::vec3> positions;
        std::vector<glm::vec2> uv;
        std::vector<glm::vec3> normals;
        std::vector<unsigned int> indices;

        const unsigned int X_SEGMENTS = 64;
        const unsigned int Y_SEGMENTS = 64;
        const float PI = 3.14159265359;
        for (unsigned int y = 0; y <= Y_SEGMENTS; ++y)
        {
            for (unsigned int x = 0; x <= X_SEGMENTS; ++x)
            {
                float xSegment = (float)x / (float)X_SEGMENTS;
                float ySegment = (float)y / (float)Y_SEGMENTS;
                float xPos = std::cos(xSegment * 2.0f * PI) * std::sin(ySegment * PI);
                float yPos = std::cos(ySegment * PI);
                float zPos = std::sin(xSegment * 2.0f * PI) * std::sin(ySegment * PI);

                positions.push_back(glm::vec3(xPos, yPos, zPos));
                uv.push_back(glm::vec2(xSegment, ySegment));
                normals.push_back(glm::vec3(xPos, yPos, zPos));
            }
        }

        bool oddRow = false;
        for (int y = 0; y < Y_SEGMENTS; ++y)
        {
            if (!oddRow) // even rows: y == 0, y == 2; and so on
            {
                for (int x = 0; x <= X_SEGMENTS; ++x)
                {
                    indices.push_back(y       * (X_SEGMENTS + 1) + x);
                    indices.push_back((y + 1) * (X_SEGMENTS + 1) + x);
                }
            }
            else
            {
                for (int x = X_SEGMENTS; x >= 0; --x)
                {
                    indices.push_back((y + 1) * (X_SEGMENTS + 1) + x);
                    indices.push_back(y       * (X_SEGMENTS + 1) + x);
                }
            }
            oddRow = !oddRow;
        }
        indexCount = indices.size();

        std::vector<float> data;
        for (int i = 0; i < positions.size(); ++i)
        {
            data.push_back(positions[i].x);
            data.push_back(positions[i].y);
            data.push_back(positions[i].z);
            if (uv.size() > 0)
            {
                data.push_back(uv[i].x);
                data.push_back(uv[i].y);
            }
            if (normals.size() > 0)
            {
                data.push_back(normals[i].x);
                data.push_back(normals[i].y);
                data.push_back(normals[i].z);
            }
        }
        glBindVertexArray(sphereVAO);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, data.size() * sizeof(float), &data[0], GL_STATIC_DRAW);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);
        float stride = (3 + 2 + 3) * sizeof(float);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, stride, (void*)(5 * sizeof(float)));
    }

    glBindVertexArray(sphereVAO);
    glDrawElements(GL_TRIANGLE_STRIP, indexCount, GL_UNSIGNED_INT, 0);
}

// utility function for loading a 2D texture from file
// ---------------------------------------------------
unsigned int loadTexture(char const * path)
{
    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
    if (data)
    {
        GLenum format;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        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_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else
    {
        std::cout << "Texture failed to load at path: " << path << std::endl;
        stbi_image_free(data);
    }

    return textureID;
}

帶貼圖的PBR

把我們系統(tǒng)擴(kuò)展成可以接受紋理作為參數(shù)可以讓我們對(duì)物體的材質(zhì)有更多的自定義空間:

[...]
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;

void main()
{
    vec3 albedo     = pow(texture(albedoMap, TexCoords).rgb, 2.2);
    vec3 normal     = getNormalFromNormalMap();
    float metallic  = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao        = texture(aoMap, TexCoords).r;
    [...]
}

不過需要注意的是一般來說反射率(albedo)紋理在美術(shù)人員創(chuàng)建的時(shí)候就已經(jīng)在sRGB空間了,因此我們需要在光照計(jì)算之前先把他們轉(zhuǎn)換到線性空間。一般來說,環(huán)境光遮蔽貼圖(ambient occlusion maps)也需要我們轉(zhuǎn)換到線性空間。不過金屬性(Metallic)和粗糙度(Roughness)貼圖大多數(shù)時(shí)間都會(huì)保證在線性空間中。

只是把之前的球體的材質(zhì)性質(zhì)換成紋理屬性,就在視覺上有巨大的提升:

你可以在這里找到紋理貼圖過的全部代碼, 以及我用的紋理(記得加上一張全白色的ao Map)。注意金屬表面會(huì)在場(chǎng)景中看起來有點(diǎn)黑,因?yàn)樗麄儧]有漫反射。它們會(huì)在考慮環(huán)境鏡面光照的時(shí)候看起來更加自然,不過這是我們下一個(gè)教程的事情了。

#include <glad/glad.h>  
#include <GLFW/glfw3.h>  
#include <stb_image.h>  
#include <glm/glm.hpp>  
#include <glm/gtc/matrix_transform.hpp>  
#include <glm/gtc/type_ptr.hpp>  
#include <[learnopengl/shader.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader.h)>  
#include <[learnopengl/camera.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/camera.h)>  
#include <[learnopengl/model.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/model.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);
unsigned int loadTexture(const char *path);
void renderSphere();

// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = 800.0f / 2.0;
float lastY = 600.0 / 2.0;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_SAMPLES, 4);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    glfwMakeContextCurrent(window);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);

    // build and compile shaders
    // -------------------------
    Shader shader("[1.2.pbr.vs](https://learnopengl.com/code_viewer_gh.php?code=src/6.pbr/1.2.lighting_textured/1.2.pbr.vs)", "[1.2.pbr.fs](https://learnopengl.com/code_viewer_gh.php?code=src/6.pbr/1.2.lighting_textured/1.2.pbr.fs)");
    shader.use();
    shader.setInt("albedoMap", 0);
    shader.setInt("normalMap", 1);
    shader.setInt("metallicMap", 2);
    shader.setInt("roughnessMap", 3);
    shader.setInt("aoMap", 4);

    // load PBR material textures
    // --------------------------
    unsigned int albedo    = loadTexture(FileSystem::getPath("resources/textures/pbr/rusted_iron/albedo.png").c_str());
    unsigned int normal    = loadTexture(FileSystem::getPath("resources/textures/pbr/rusted_iron/normal.png").c_str());
    unsigned int metallic  = loadTexture(FileSystem::getPath("resources/textures/pbr/rusted_iron/metallic.png").c_str());
    unsigned int roughness = loadTexture(FileSystem::getPath("resources/textures/pbr/rusted_iron/roughness.png").c_str());
    unsigned int ao        = loadTexture(FileSystem::getPath("resources/textures/pbr/rusted_iron/ao.png").c_str());

    // lights
    // ------
    glm::vec3 lightPositions[] = {
        glm::vec3(0.0f, 0.0f, 10.0f),
    };
    glm::vec3 lightColors[] = {
        glm::vec3(150.0f, 150.0f, 150.0f),
    };
    int nrRows = 7;
    int nrColumns = 7;
    float spacing = 2.5;

    // initialize static shader uniforms before rendering
    // --------------------------------------------------
    glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    shader.use();
    shader.setMat4("projection", projection);

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

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

        // render
        // ------
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        shader.use();
        glm::mat4 view = camera.GetViewMatrix();
        shader.setMat4("view", view);
        shader.setVec3("camPos", camera.Position);

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, albedo);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, normal);
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_2D, metallic);
        glActiveTexture(GL_TEXTURE3);
        glBindTexture(GL_TEXTURE_2D, roughness);
        glActiveTexture(GL_TEXTURE4);
        glBindTexture(GL_TEXTURE_2D, ao);

        // render rows*column number of spheres with material properties defined by textures (they all have the same material properties)
        glm::mat4 model;
        for (unsigned int row = 0; row < nrRows; ++row)
        {
            for (unsigned int col = 0; col < nrColumns; ++col)
            {
                model = glm::mat4();
                model = glm::translate(model, glm::vec3(
                    (float)(col - (nrColumns / 2)) * spacing,
                    (float)(row - (nrRows / 2)) * spacing,
                    0.0f
                ));
                shader.setMat4("model", model);
                renderSphere();
            }
        }

        // render light source (simply re-render sphere at light positions)
        // this looks a bit off as we use the same shader, but it'll make their positions obvious and 
        // keeps the codeprint small.
        for (unsigned int i = 0; i < sizeof(lightPositions) / sizeof(lightPositions[0]); ++i)
        {
            glm::vec3 newPos = lightPositions[i] + glm::vec3(sin(glfwGetTime() * 5.0) * 5.0, 0.0, 0.0);
            newPos = lightPositions[i];
            shader.setVec3("lightPositions[" + std::to_string(i) + "]", newPos);
            shader.setVec3("lightColors[" + std::to_string(i) + "]", lightColors[i]);

            model = glm::mat4();
            model = glm::translate(model, newPos);
            model = glm::scale(model, glm::vec3(0.5f));
            shader.setMat4("model", model);
            renderSphere();
        }

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 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;
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.ProcessKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.ProcessKeyboard(BACKWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.ProcessKeyboard(LEFT, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.ProcessKeyboard(RIGHT, deltaTime);
}

// 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; // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(yoffset);
}

// renders (and builds at first invocation) a sphere
// -------------------------------------------------
unsigned int sphereVAO = 0;
unsigned int indexCount;
void renderSphere()
{
    if (sphereVAO == 0)
    {
        glGenVertexArrays(1, &sphereVAO);

        unsigned int vbo, ebo;
        glGenBuffers(1, &vbo);
        glGenBuffers(1, &ebo);

        std::vector<glm::vec3> positions;
        std::vector<glm::vec2> uv;
        std::vector<glm::vec3> normals;
        std::vector<unsigned int> indices;

        const unsigned int X_SEGMENTS = 64;
        const unsigned int Y_SEGMENTS = 64;
        const float PI = 3.14159265359;
        for (unsigned int y = 0; y <= Y_SEGMENTS; ++y)
        {
            for (unsigned int x = 0; x <= X_SEGMENTS; ++x)
            {
                float xSegment = (float)x / (float)X_SEGMENTS;
                float ySegment = (float)y / (float)Y_SEGMENTS;
                float xPos = std::cos(xSegment * 2.0f * PI) * std::sin(ySegment * PI);
                float yPos = std::cos(ySegment * PI);
                float zPos = std::sin(xSegment * 2.0f * PI) * std::sin(ySegment * PI);

                positions.push_back(glm::vec3(xPos, yPos, zPos));
                uv.push_back(glm::vec2(xSegment, ySegment));
                normals.push_back(glm::vec3(xPos, yPos, zPos));
            }
        }

        bool oddRow = false;
        for (int y = 0; y < Y_SEGMENTS; ++y)
        {
            if (!oddRow) // even rows: y == 0, y == 2; and so on
            {
                for (int x = 0; x <= X_SEGMENTS; ++x)
                {
                    indices.push_back(y       * (X_SEGMENTS + 1) + x);
                    indices.push_back((y + 1) * (X_SEGMENTS + 1) + x);
                }
            }
            else
            {
                for (int x = X_SEGMENTS; x >= 0; --x)
                {
                    indices.push_back((y + 1) * (X_SEGMENTS + 1) + x);
                    indices.push_back(y       * (X_SEGMENTS + 1) + x);
                }
            }
            oddRow = !oddRow;
        }
        indexCount = indices.size();

        std::vector<float> data;
        for (int i = 0; i < positions.size(); ++i)
        {
            data.push_back(positions[i].x);
            data.push_back(positions[i].y);
            data.push_back(positions[i].z);
            if (uv.size() > 0)
            {
                data.push_back(uv[i].x);
                data.push_back(uv[i].y);
            }
            if (normals.size() > 0)
            {
                data.push_back(normals[i].x);
                data.push_back(normals[i].y);
                data.push_back(normals[i].z);
            }
        }
        glBindVertexArray(sphereVAO);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, data.size() * sizeof(float), &data[0], GL_STATIC_DRAW);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);
        float stride = (3 + 2 + 3) * sizeof(float);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, stride, (void*)(5 * sizeof(float)));
    }

    glBindVertexArray(sphereVAO);
    glDrawElements(GL_TRIANGLE_STRIP, indexCount, GL_UNSIGNED_INT, 0);
}

// utility function for loading a 2D texture from file
// ---------------------------------------------------
unsigned int loadTexture(char const * path)
{
    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
    if (data)
    {
        GLenum format;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        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_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else
    {
        std::cout << "Texture failed to load at path: " << path << std::endl;
        stbi_image_free(data);
    }

    return textureID;
}

相比起在網(wǎng)上找到的其他PBR渲染結(jié)果來說,盡管在視覺上不算是非常震撼,因?yàn)槲覀冞€沒考慮到基于圖片的關(guān)照,IBL。我們現(xiàn)在也算是有了一個(gè)基于物理的渲染器了(雖然還沒考慮IBL)!你會(huì)發(fā)現(xiàn)你的光照看起來更加真實(shí)了。

譯者注:
本章教程有幾個(gè)小坑原作者沒有說清楚,可能是希望讀者自己思考,在這譯者稍稍提醒一下:

  • 首先是球體的生成,主流的球體頂點(diǎn)生成有兩種方法,作者源碼采用的是UVSphere方法, IcoSpher方法可以參考這里 。
  • 對(duì)于貼圖的PBR來說,我們需要TBN矩陣做坐標(biāo)轉(zhuǎn)換(切線空間-> 世界空間 或者 世界空間 -> 切線空間,參考 法線貼圖 章節(jié)。)。這有兩種方法,一種是在片段著色器中使用叉乘計(jì)算TBN矩陣(作者采用的方法);另外一種是在根據(jù)頂點(diǎn)預(yù)計(jì)算TBN然后VAO中傳入TBN矩陣,理論上來說后者會(huì)比較快(但是比較麻煩),不過在譯者的實(shí)際測(cè)試中兩者速度差距不大。

后記

本篇已結(jié)束,下一篇是PBR - IBL。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容