opengl 教程(17) 环境光
原帖地址:http://ogldev.atspace.co.uk/www/tutorial17/tutorial17.html
在3D真实感图形学中,光照是很重要的技术。从物理上讲,一束光是由很多细小的粒子“光子”组成,这些光子在空气中传输,在物体的表面折射,反射,最终进入我的视觉系统,形成了我们眼中看到的真实世界。在编程中,我们不可能模拟所有光子的行为,所以如何对光照进行建模,模拟出它的真实效果,是计算机图形学中一个永恒的话题
在各种paper中,人们已经提出了许多种光照模型算法,随着图形学的发展以及GPU计算能力提高,还会出现更多的光照算法。在本篇教程中,我们只讨论最基本的光照模型,它编程相对容易,它的效果作用于场景中的所有物体。
最基本的光照模型称作 环境光/漫反射光/高光(Ambient/Diffuse/Specular)光照模型:
环境光就是在没有太阳的情况下,你周围的光照情况,它模拟的是太阳光照射地面后,在不同的位置被反射,折射,最后混合在一起的一种光线,环境光没有方向,没有起点,它是一种均匀的光照效果,即使在阴影中,也有环境光存在。
漫反射光是光照在物体表面被反射,最终进入我们眼睛的效果,漫反射光和光线的方向,以及物体表面的法线有关,比如一个物体有2面,当漫反射光只照在一个面时,这个面是亮的,而另一个面是暗的。太阳光看似没有方向,其实是有的,比如一个大楼,太阳照在它的上面,它的投影方向就是太阳光的方向。
高光是物体本身的一种属性,当光作用于物体时,物体上某个部分可能特别亮,这就是高光,高光会随着我们的视点移动而不断改变位置。金属物体一般都有这种高光属性,比如金属球,汽车表面等等。计算高光必须考虑光线方向,光照物体的法向,以及视线的方向。
在3D应用程序中,我们并不直接设定环境光,漫反射光和高光,而是使用光源的概念,比如太阳,电灯,蜡烛,火把等等,这些光源都是环境光,漫反射光,高光不同强度的绑定组合。
在下面的教程中,我们实现几种不同光源,通过这些光源来研究基本光照模型。
首先我们来看看“方向光”,方向光是一束没有起点的光,这意味着所有的光线都是平行的。方向光的方向可以用一个向量来表示,这个向量用来计算场景中所有物体的光照效果。现实生活中,太阳光就可以认为是方向光,并不是说太阳光发出的光线都是平行的,只是因为太阳离我们太远了,它的光线可以近似认为是平行光。
方向光另外一个重要的属性就是不管它离物体的距离多远,它的亮度都是恒定的,在下面的教程,我们将会学习到点光源,而点光源的亮度是会随着距离衰减的。
下面的图很好的解释了方向光:
我们已经知道太阳光包括环境光和漫反射光2部分,本篇教程中,我们先编程实现环境光的效果,下一篇教程再来学习漫反射光。
前面的教程中,我们学习了如何在一个纹理上采样像素颜色,像素颜色有3个通道,红绿蓝(RGB),每个通道都是一个单字节,也就是说每个通道的颜色值范围是[0,255],不同的RGB值代表不同的颜色,(0,0,0)表示黑色,(255,255,255)表示白色。我们可以对一个颜色RGB值按比例缩放,可以看到颜色不变,但亮度越来越低。er or darker (depending on the scaling factor)。
白色光照到物体表面时候,反射的光仅仅是物体本身的颜色,可能会更暗或者更亮,这依赖于光源的强度。如果光源是纯红色(255,0,0),那么反射光就仅仅是物体表面的颜色的红色通道部分,因为没有蓝色和绿色部分被反射,如果物体是纯蓝的,那么此时物体看起来就是黑色的,因为没有蓝色光被反射。
我们将指定光源颜色的RGB值范围是[0-1],然后把光源颜色和物体表面颜色相乘,得到光反射的颜色,即我们看到的物体颜色。下面的方程用来计算环境光:
在这篇教程程序中,我们可以通过a和s键来增加和减少环境光的强度,当然,这只是环境光的部分,方向光本身还没有被计算进来,在下篇教程中,我们将来实现漫反射光,现在我们渲染的物体金字塔,它的四个面光照都是一样的。
环境光真实感并不强,是人为添加的光照因素,后面我们考虑了漫反射和高光后,会把它们组合在一起,实现更真实的三维物体渲染。
代码如下:
本教程中,我们会对程序架构进行一些改变,主要变化如下:
- 在 Technique 类中包装了shader管理,比如shader编译,链接等等。我们自己的特效(effect)类都是从 Technique 中派生出来的。
- 把 GLUT初始化以及回调函数管理放在GLUTBackend模块中处理,这个模块会注册自己以便接受GLUT的回调函数,然后通过C++接口类 ICallbacks 转发给应用程序。
- 把主cpp中的全局函数和变量移动到一个类中,这个类能够被看作应用程序类,在后面的教程中,我们将会扩展这个类,提供更多的通用功能,这中模式在很多的引擎和框架中都很流行。
下面看看重构后的代码:
glut_backend.h
void GLUTBackendInit(int argc, char** argv);
bool GLUTBackendCreateWindow(unsigned int Width, unsigned int Height, unsigned int bpp, bool isFullScreen, const char* pTitle);
GLUT相关的代码都被移到 "GLUT backend" 模块中,这使得初始化GLUT和创建窗口变得更容易。
void GLUTBackendRun(ICallbacks* pCallbacks);
GLUT初始化和创建窗口后,我们就要执行GLUT主循环,GLUTBackendRun函数就是用来执行主循环的。新增的Callbacks接口用来注册GLUT回调函数。和前面教程中每个应用程序注册自己的回调函数不同,GLUT backend模块把自己注册为虚函数函数,然后通过继承类,把事件发送到这个函数中掉用的对象。
technique.h
class Technique
{
public:
Technique();
~Technique();
virtual bool Init();
void Enable();
protected:
bool AddShader(GLenum ShaderType, const char* pShaderText);
bool Finalize();
GLint GetUniformLocation(const char* pUniformName);
private:
GLuint m_shaderProg;
typedef std::list<GLuint> ShaderObjList;
ShaderObjList m_shaderObjList;
};
前面教程中,我们在主程序cpp中实现shader的编译,链接等功能,本教程代码中,我们包装了这些公用的功能,建立Technique类,它的继承类将主要专注于shader特效。
每个technique首先要调用Init函数进行实例化操作,techniqu的派生类必须首先调用基类的Init化函数(该函数中创建OpenGL程序对象),也可以增加自己的私有函数。
一个Technique对象被创建和实例化后,然后派生类要调用函数 AddShader() 去增加shader,最后调用Finalize()函数链接对象。在切换 technique和调用draw之前,我们必须调用Enable函数,因为该函数包装了 glUseProgram() 。
该类会跟踪中间编译的对象,并在它们使用完毕后,用glDeleteShader()删除它们,这有助于减少程序的资源消耗。程序对象本身在析构函数通过glDeleteProgram()删除。
tutorial17.cpp
class Tutorial17 : public ICallbacks
{
public:
Tutorial17()
{
...
}
~Tutorial17()
{
...
}
bool Init()
{
...
}
void Run()
{
GLUTBackendRun(this);
}
virtual void RenderSceneCB()
{
...
}
virtual void IdleCB()
{
...
}
virtual void SpecialKeyboardCB(int Key, int x, int y)
{
...
}
virtual void KeyboardCB(unsigned char Key, int x, int y)
{
...
}
virtual void PassiveMouseCB(int x, int y)
{
...
}
private:
void CreateVertexBuffer()
{
...
}
void CreateIndexBuffer()
{
...
}
GLuint m_VBO;
GLuint m_IBO;
LightingTechnique* m_pEffect;
Texture* m_pTexture;
Camera* m_pGameCamera;
float m_scale;
DirectionalLight m_directionalLight;
};
上面的代码是主应用程序类的代码,它封装了其余的一些代码。Init() 用于创建特效类,装入纹理和创建顶点和索引缓冲,Run函数调用 GLUTBackendRun()并把对象自己作为参数传入
lighting_technique.h
struct DirectionalLight
{
Vector3f Color;
float AmbientIntensity;
};
我们定义了一个方向光的结构,注意现在成员只有光的颜色和环境光,后面的教程中,我们会继续增加漫反射和高光部分。
第一个参数是光源的RGA颜色,第二参数是环境光系数,为1.0时候,是很亮的环境光,为0.1时候是很暗的环境光。
class LightingTechnique : public Technique
{
public:
LightingTechnique();
virtual bool Init();
void SetWVP(const Matrix4f& WVP);
void SetTextureUnit(unsigned int TextureUnit);
void SetDirectionalLight(const DirectionalLight& Light);
private:
GLuint m_WVPLocation;
GLuint m_samplerLocation;
GLuint m_dirLightColorLocation;
GLuint m_dirLightAmbientIntensityLocation;
};
我们将从 Technique类中派生出光照类,该类创建后,首先要调用Init函数。
in vec2 TexCoord0;
out vec4 FragColor;
struct DirectionalLight
{
vec3 Color;
float AmbientIntensity;
};
uniform DirectionalLight gDirectionalLight;
uniform sampler2D gSampler;
void main()
{
FragColor = texture2D(gSampler, TexCoord0.xy) *
vec4(gDirectionalLight.Color, 1.0f) *
gDirectionalLight.AmbientIntensity;
}
顶点shader代码没有变化,片元shader中,增加了一个方向光的uniform变量,该变量用来从应用程序传入光照参数。最后的返回颜色,我们用光照和纹理采样的结果相乘,得到最终的像素颜色。
m_WVPLocation = GetUniformLocation("gWVP");
m_samplerLocation = GetUniformLocation("gSampler");
m_dirLightColorLocation = GetUniformLocation("gDirectionalLight.Color");
m_dirLightAmbientIntensityLocation = GetUniformLocation("gDirectionalLight.AmbientIntensity");
为了访问uniform变量DirectionalLight,我们增加了上述代码,注意,对于结构中成员变量,我们需要分别指定。
程序运行后,界面如下,我们可以用a和s键来调节环境光亮度。