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

版本記錄

版本號 時間
V1.0 2018.01.20

前言

OpenGL 圖形庫項目中一直也沒用過蜀涨,最近也想學(xué)著使用這個圖形庫只祠,感覺還是很有意思兜蠕,也就自然想著好好的總結(jié)一下,希望對大家能有所幫助抛寝。下面內(nèi)容來自歡迎來到OpenGL的世界熊杨。
1. OpenGL 圖形庫使用(一) —— 概念基礎(chǔ)
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象盗舰、擴(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 圖形庫的使用(二十一)—— 高級OpenGL之深度測試
22. OpenGL 圖形庫的使用(二十二)—— 高級OpenGL之模板測試Stencil testing
23. OpenGL 圖形庫的使用(二十三)—— 高級OpenGL之混合Blending
24. OpenGL 圖形庫的使用(二十四)—— 高級OpenGL之面剔除Face culling
25. OpenGL 圖形庫的使用(二十五)—— 高級OpenGL之幀緩沖Framebuffers
26. OpenGL 圖形庫的使用(二十六)—— 高級OpenGL之立方體貼圖Cubemaps
27. OpenGL 圖形庫的使用(二十七)—— 高級OpenGL之高級數(shù)據(jù)Advanced Data
28. OpenGL 圖形庫的使用(二十八)—— 高級OpenGL之高級GLSL Advanced GLSL
29. OpenGL 圖形庫的使用(二十九)—— 高級OpenGL之幾何著色器Geometry Shader
30. OpenGL 圖形庫的使用(三十)—— 高級OpenGL之實(shí)例化Instancing
31. OpenGL 圖形庫的使用(三十一)—— 高級OpenGL之抗鋸齒Anti Aliasing
32. OpenGL 圖形庫的使用(三十二)—— 高級光照之高級光照Advanced Lighting
33. OpenGL 圖形庫的使用(三十三)—— 高級光照之Gamma校正Gamma Correction
34. OpenGL 圖形庫的使用(三十四)—— 高級光照之陰影 - 陰影映射Shadow Mapping
35. OpenGL 圖形庫的使用(三十五)—— 高級光照之陰影 - 點(diǎn)陰影Point Shadows
36. OpenGL 圖形庫的使用(三十六)—— 高級光照之法線貼圖Normal Mapping
37. OpenGL 圖形庫的使用(三十七)—— 高級光照之視差貼圖Parallax Mapping
38. OpenGL 圖形庫的使用(三十八)—— 高級光照之HDR
39. OpenGL 圖形庫的使用(三十九)—— 高級光照之泛光
40. OpenGL 圖形庫的使用(四十)—— 高級光照之延遲著色法Deferred Shading
41. OpenGL 圖形庫的使用(四十一)—— 高級光照之SSAO
42. OpenGL 圖形庫的使用(四十二)—— PBR之理論Theory

光照

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

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

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

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

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

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

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

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

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

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

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;

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

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

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

這也讓我們回到了對于表面的半球領(lǐng)域(hemisphere)Ω的積分∫上尺铣。由于我們事先知道的所有貢獻(xiàn)光源的位置拴曲,因此對物體表面上的一個點(diǎn)著色并不需要我們嘗試去求解積分。我們可以直接拿光源的(已知的)數(shù)目凛忿,去計算它們的總輻照度澈灼,因為每個光源僅僅只有一個方向上的光線會影響物體表面的輻射率。這使得PBR對直接光源的計算相對簡單,因為我們只需要有效地遍歷所有有貢獻(xiàn)的光源。而當(dāng)我們后來把環(huán)境照明也考慮在內(nèi)的IBL教程中,我們就必須采取積分去計算了咐蚯,這是因為光線可能會在任何一個方向入射吃环。


一個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ù)。

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

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

直接光照明

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

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ìn)行Gamma校正)泳姐,我們使用在物理上更為準(zhǔn)確的平方倒數(shù)作為衰減效拭。

相對于物理上正確來說,你可能仍然想使用常量胖秒,線性或者二次衰減方程(他們在物理上相對不準(zhǔn)確)缎患,卻可以為您提供在光的能量衰減更多的控制。

然后阎肝,對于每一個光源我們都想計算完整的Cook-Torrance specular BRDF項:

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

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

菲涅爾方程返回的是一個物體表面光線被反射的百分比风题, 也就是我們反射方程中的參數(shù)ks判导。Fresnel-Schlick近似接受一個參數(shù)F0,被稱為0°入射角的反射(surface reflection at zero incidence)表示如果直接(垂直)觀察表面的時候有多少光線會被反射沛硅。 這個參數(shù)F0會因為材料不同而不同眼刃,而且會因為材質(zhì)是金屬而發(fā)生變色。在PBR金屬流中我們簡單地認(rèn)為大多數(shù)的絕緣體在F0為0.04的時候看起來視覺上是正確的摇肌,我們同時會特別指定F0當(dāng)我們遇到金屬表面并且給定反射率的時候擂红。 因此代碼上看起來會像是這樣:

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

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

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

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

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

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

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

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

這樣我們就湊夠了足夠的項來計算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;  

注意我們在分母項中加了一個0.001為了避免出現(xiàn)除零錯誤。

現(xiàn)在我們終于可以計算每個光源在反射率方程中的貢獻(xiàn)值了饺窿!因為菲涅爾方程直接給出了kS歧焦, 我們可以使用F表示鏡面反射在所有打在物體表面上的光線的貢獻(xiàn)。 從kS我們很容易計算折射的比值kD

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

kD *= 1.0 - metallic;   

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

    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í)際上不需要去求積宇弛,因為對于所有可能的入射光線方向我們知道只有4個方向的入射光線會影響片段(像素)的著色鸡典。因為這樣,我們可以直接循環(huán)N次計算這些入射光線的方向(N也就是場景中光源的數(shù)目)枪芒。

比較重要的是我們沒有把kS乘進(jìn)去我們的反射率方程中彻况,這是因為我們已經(jīng)在specualr BRDF中乘了菲涅爾系數(shù)F了,因為kS等于F舅踪,因此我們不需要再乘一次纽甘。

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

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

線性空間和HDR渲染

直到現(xiàn)在硫朦,我們假設(shè)的所有計算都在線性的顏色空間中進(jìn)行的贷腕,因此我們需要在著色器最后做伽馬矯正背镇。 在線性空間中計算光照是非常重要的咬展,因為PBR要求所有輸入都是線性的,如果不是這樣瞒斩,我們就會得到不正常的光照破婆。另外,我們希望所有光照的輸入都盡可能的接近他們在物理上的取值胸囱,這樣他們的反射率或者說顏色值就會在色譜上有比較大的變化空間祷舀。Lo作為結(jié)果可能會變大得很快(超過1),但是因為默認(rèn)的LDR輸入而取值被截斷。所以在伽馬矯正之前我們采用色調(diào)映射使Lo從LDR的值映射為HDR的值裳扯。

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

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

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


完整的直接光照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)過上一個教程的理論知識以及學(xué)習(xí)過關(guān)于渲染方程的一些知識后圾叼,這個著色器看起來不會太可怕。如果我們采用這個著色器扔仓,加上4個點(diǎn)光源和一些球體褐奥,同時我們令這些球體的金屬性(metallic)和粗糙度(roughness)沿垂直方向和水平方向分別變化,我們會得到這樣的結(jié)果:

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

你可以在這里找到整個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ù)可以讓我們對物體的材質(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)建的時候就已經(jīng)在sRGB空間了,因此我們需要在光照計算之前先把他們轉(zhuǎn)換到線性空間森篷。一般來說输钩,環(huán)境光遮蔽貼圖(ambient occlusion maps)也需要我們轉(zhuǎn)換到線性空間。不過金屬性(Metallic)和粗糙度(Roughness)貼圖大多數(shù)時間都會保證在線性空間中仲智。

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

你可以在這里找到紋理貼圖過的全部代碼, 以及我用的紋理(記得加上一張全白色的ao Map)钓辆。注意金屬表面會在場景中看起來有點(diǎn)黑剪验,因為他們沒有漫反射肴焊。它們會在考慮環(huán)境鏡面光照的時候看起來更加自然,不過這是我們下一個教程的事情了功戚。

#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é)果來說娶眷,盡管在視覺上不算是非常震撼,因為我們還沒考慮到基于圖片的關(guān)照,IBL啸臀。我們現(xiàn)在也算是有了一個基于物理的渲染器了(雖然還沒考慮IBL)茂浮!你會發(fā)現(xiàn)你的光照看起來更加真實(shí)了。

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

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

后記

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市万栅,隨后出現(xiàn)的幾起案子烦粒,更是在濱河造成了極大的恐慌,老刑警劉巖义黎,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異狐蜕,居然都是意外死亡层释,警方通過查閱死者的電腦和手機(jī)廉白,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門猴蹂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人聋溜,你說我怎么就攤上這事勤婚。” “怎么了祝迂?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長纠俭。 經(jīng)常有香客問我冤荆,道長,這世上最難降的妖魔是什么外邓? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任损话,我火速辦了婚禮丧枪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屎篱。我一直安慰自己,他們只是感情好秦士,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著曹傀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪幕庐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天届吁,我揣著相機(jī)與錄音疚沐,去河邊找鬼亮蛔。 笑死辣吃,一個胖子當(dāng)著我的面吹牛神得,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播节榜,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼别智,長吁一口氣:“原來是場噩夢啊……” “哼宗苍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起薄榛,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤讳窟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后敞恋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挪钓,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡葱弟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出弊予,到底是詐尸還是另有隱情碾褂,我是刑警寧澤咒程,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布呢铆,位于F島的核電站,受9級特大地震影響周偎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜助琐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望趾娃。 院中可真熱鬧,春花似錦上祈、人聲如沸揍很。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽线召。三九已至,卻和暖如春罢维,著一層夾襖步出監(jiān)牢的瞬間瑰艘,已是汗流浹背材彪。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留琴儿,地道東北人段化。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像凤类,于是被迫代替她去往敵國和親穗泵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容