到目前为止我们的渲染管线已经基本为完整了,从模型加载到顶点着色器,再经过裁剪到片元着色器,但在片元着色器中我们目前还只是输出顶点颜色或者纹理值,还没有加入光照计算,这一节我们就来加入光照的计算。最简单的 Blinn Phong 模型我们已经非常熟悉了,所以这一节的重点并不是光照的计算,而是了解三种光源是如何实现的。
1 光源概述 在 RTR 一书中,作者将光源分为三种,分别是:平行光(direction)、点光源(point)和聚光灯(spot)。我们分别来实现这三种光源。
2 准备工作 在实现光源之前我们需要一些函数和变量,首先是在 Global.h
中定义的环境光项:
1 2 const glm::vec3 Ambient = glm::vec3 (0.5 , 0.5 , 0.5 );
还有计算反射方向的函数:
1 2 3 4 5 6 glm::vec3 reflect (const glm::vec3& lightDir, const glm::vec3& normal) { return lightDir - 2 * glm::dot (normal, lightDir) * normal; }
3 平行光 在实现各类光源之前我们先实现一个光源基类,这样其他光源都派生自该基类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Light {public : glm::vec3 Position; glm::vec3 Color; glm::vec3 Specular; glm::vec3 Direction; float Intensity; virtual ~Light () { } Light ( const glm::vec3& pos = glm::vec3 (0 , 0 , 0 ), const glm::vec3& color = glm::vec3 (1 , 1 , 1 ), const glm::vec3& specular = glm::vec3 (1 , 1 , 1 ), const glm::vec3& dir = glm::vec3 (0 , -1 , 0 ), const float & i = 1.0f ) : Position (pos), Color (color), Specular (specular), Direction (dir), Intensity (i) {} };
然后是平行光,平行光不需要位置:
1 2 3 4 5 6 7 8 9 10 11 class DirectionLight : public Light {public : DirectionLight ( const glm::vec3& dir = glm::normalize (glm::vec3 (0 , -1 , 1 )), const glm::vec3& color = glm::vec3 (1 , 1 , 1 ), const glm::vec3& specular = glm::vec3 (1 , 1 , 1 ), const float & i = 1.0f ) : Light (glm::vec3 (0 , 0 , 0 ), color, specular, dir, i) {} };
然后我们新建一个 BlinnPhongShader
类继承于基本的 Shader 类,在其中实现平行光的计算,并修改片元着色器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #pragma once #ifndef BLINNPHONG_H #define BLINNPHONG_H #include "Shader.h" #include "Light.h" #include "Material.h" #include "Camera.h" class BlinnPhongShader : public Shader {public : BlinnPhongShader () = default ; virtual ~BlinnPhongShader () = default ; static glm::vec3 CalcDirLight ( const DirectionLight& dirLight, const glm::vec3& worldNormal, const glm::vec3& worldViewDir, const glm::vec3& albedo ) { float diff = max (glm::dot (worldNormal, -dirLight.Direction), 0 ); glm::vec3 halfDir = glm::normalize (worldViewDir - dirLight.Direction); float spec = pow (max (glm::dot (halfDir, worldNormal), 0 ), currentMat->Gloss); glm::vec3 diffuse = dirLight.Color * diff * albedo; glm::vec3 specular = dirLight.Specular * spec; return (diffuse + specular) * dirLight.Intensity; } glm::vec4 FragmentShader (const V2F& v) { glm::vec3 albedo = texture->Sample2D (v.texcoord) * currentMat->Color; glm::vec3 worldNormal = glm::normalize (v.normal); glm::vec3 worldViewDir = glm::normalize (camera->Position - glm::vec3 (v.worldPos)); glm::vec3 result = Ambient * albedo; for (int i = 0 ; i < dirLtNums; i++) result += BlinnPhongShader::CalcDirLight (*(dirLights + i), worldNormal, worldViewDir, albedo); return glm::vec4 (result, 1.0 ); } }; #endif
然后在主函数中加入一个平行光:
1 2 3 4 5 6 7 8 9 10 DirectionLight dir (glm::vec3(0 , 0 , 1 ), glm::vec3(1 , 1 , 1 ), glm::vec3(1 , 1 , 1 ), 1.0 ) ;dirLights = &dir; dirLtNums = 1 ; BlinnPhongShader shader; ...
查看效果:
4 点光源 点光源无所谓方向,它向四周均匀发光,光照强度和距离成反比,我们使用常数项、一次项和二次项来控制光照衰减,首先是点光源类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class PointLight : public Light {public : float Constant; float Linear; float Quadratic; PointLight ( const glm::vec3& pos = glm::vec3 (0 , 0 , 0 ), const glm::vec3& color = glm::vec3 (1 , 1 , 1 ), const glm::vec3& specular = glm::vec3 (1 , 1 , 1 ), const float & i = 1.0f , const float & c = 1.0f , const float & l = 0.09f , const float & q = 0.032f ) : Light (pos, color, specular, glm::vec3 (0 , 0 , 0 ), i), Constant (c), Linear (l), Quadratic (q) {} };
然后是计算点光源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static glm::vec3 CalcPtLight ( const PointLight& ptLight, const glm::vec3& worldPos, const glm::vec3& worldNormal, const glm::vec3& worldViewDir, const glm::vec3& albedo ) { float distance = glm::distance (worldPos, ptLight.Position); float attenuation = 1.0 / (ptLight.Constant + ptLight.Linear * distance + ptLight.Quadratic * (distance * distance)); glm::vec3 lightDir = glm::normalize (worldPos - ptLight.Position); float diff = max (glm::dot (worldNormal, -lightDir), 0 ); glm::vec3 halfDir = glm::normalize (worldViewDir - lightDir); float spec = pow (max (glm::dot (halfDir, worldNormal), 0 ), currentMat->Gloss); glm::vec3 diffuse = ptLight.Color * diff * albedo; glm::vec3 specular = ptLight.Specular * spec; diffuse *= attenuation; specular *= attenuation; return (diffuse + specular) * ptLight.Intensity; }
在片元着色器中加入点光源贡献:
1 2 3 4 5 6 7 8 9 10 11 12 13 glm::vec4 FragmentShader (const V2F& v) { glm::vec3 albedo = texture->Sample2D (v.texcoord) * currentMat->Color; glm::vec3 worldNormal = glm::normalize (v.normal); glm::vec3 worldViewDir = glm::normalize (camera->Position - glm::vec3 (v.worldPos)); glm::vec3 result = Ambient * albedo; for (int i = 0 ; i < dirLtNums; i++) result += BlinnPhongShader::CalcDirLight (*(dirLights + i), worldNormal, worldViewDir, albedo); for (int i = 0 ; i < ptLtNums; i++) result += BlinnPhongShader::CalcPtLight (*(ptLights + i), glm::vec3 (v.worldPos), worldNormal, worldViewDir, albedo); return glm::vec4 (result, 1.0 ); }
然后在主函数中加入一个点光源:
1 2 3 PointLight pt (glm::vec3(1 , 1 , 1 )) ;ptLights = &pt; ptLtNums = 1 ;
查看效果:
5 聚光灯 聚光灯可以理解为手电筒,只有在与手电筒正方向夹角在一定范围内的像素才会被照亮。像素到光源位置的方向与光源正向夹角被称为切光角,切光角在设定范围内的像素,按照点光源的方式计算光照。切光角之外的像素不会被照亮。
为了避免边缘突变,可以设立一个外切光角,在内外切光角之间使用插值乘以计算结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class SpotLight : public PointLight {public : float innerCutOff; float outterCutOff; SpotLight ( const glm::vec3& pos = glm::vec3 (0 , 0 , 0 ), const glm::vec3& dir = glm::vec3 (0 , 0 , -1 ), const glm::vec3& color = glm::vec3 (1 , 1 , 1 ), const glm::vec3& specular = glm::vec3 (1 , 1 , 1 ), const float & i = 1.0f , const float & c = 1.0f , const float & l = 0.09f , const float & q = 0.032f , const float & icut = glm::cos (glm::radians (12.5f )), const float & ocut = glm::cos (glm::radians (17.5 )) ) : PointLight (pos, color, specular, i, c, l, q), innerCutOff (icut), outterCutOff (ocut) { Direction = dir; } };
计算聚光灯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 static glm::vec3 CalcSpLight ( const SpotLight& spLight, const glm::vec3& worldPos, const glm::vec3& worldNormal, const glm::vec3& worldViewDir, const glm::vec3& albedo ) { glm::vec3 lightDir = glm::normalize (worldPos - spLight.Position); float theta = glm::dot (lightDir, glm::normalize (spLight.Direction)); float weight = saturate ((theta - spLight.outterCutOff) / (spLight.innerCutOff - spLight.outterCutOff)); float intensity = Lerp (0 , 1 , weight); float distance = glm::distance (worldPos, spLight.Position); float attenuation = 1.0 / (spLight.Constant + spLight.Linear * distance + spLight.Quadratic * (distance * distance)); float diff = max (glm::dot (worldNormal, -lightDir), 0 ); glm::vec3 halfDir = glm::normalize (worldViewDir - lightDir); float spec = pow (max (glm::dot (halfDir, worldNormal), 0 ), currentMat->Gloss); glm::vec3 diffuse = spLight.Color * diff * albedo; glm::vec3 specular = spLight.Specular * spec; diffuse *= (attenuation * intensity); specular *= (attenuation * intensity); return (diffuse + specular) * spLight.Intensity; }
在片元着色器中加入聚光灯贡献:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 glm::vec4 FragmentShader (const V2F& v) { glm::vec3 albedo = texture->Sample2D (v.texcoord) * currentMat->Color; glm::vec3 worldNormal = glm::normalize (v.normal); glm::vec3 worldViewDir = glm::normalize (camera->Position - glm::vec3 (v.worldPos)); glm::vec3 result = Ambient * albedo; for (int i = 0 ; i < dirLtNums; i++) result += BlinnPhongShader::CalcDirLight (*(dirLights + i), worldNormal, worldViewDir, albedo); for (int i = 0 ; i < ptLtNums; i++) result += BlinnPhongShader::CalcPtLight (*(ptLights + i), glm::vec3 (v.worldPos), worldNormal, worldViewDir, albedo); for (int i = 0 ; i < spLtNums; i++) result += BlinnPhongShader::CalcSpLight (*(spLights + i), glm::vec3 (v.worldPos), worldNormal, worldViewDir, albedo); return glm::vec4 (result, 1.0 ); }
在主函数中加入聚光灯:
1 2 3 4 SpotLight sp (camera->Position, camera->Front) ;spLights = &sp; spLtNums = 1 ;
最终的效果: