OpenGL教程翻译 第十七课 环境光(Ambient
Lighting)
原文地址:http://ogldev.atspace.co.uk/(源码请从原文主页下载)
Background
光照是3D图形学的最重要的研究领域之一。对光照得体的模拟会使渲染的场景增加很多视觉吸引力。之所以使用“模拟”这个词是因为你无法准确的模仿出光照在自然界中的样子。真正的光是由大量的称为“光子”的粒子组成的,并且同时表现出波和粒子的特性(光的“波粒二象性”)。如果你在程序中试图序计算每一个光子的作用,那么你将很快用光电脑的计算力。
因此,这些年来开发了一些光照模型,它们捕捉了光照射在物体上使它们可见而产生的核心效果。随着3D图形学领域的发展和计算机计算能力的提高,这些光照模型变得越来越复杂。在接下来的几章教程中我们将完成对基础光照模型的学习,这些模型虽然实现起来较为简单,不过对场景的整体氛围有极大的贡献。
基本的光照模型叫做“环境光/漫反射光/镜面反射光”(‘Ambient/Diffuse/Specular‘)。环境光是当你在平常晴天时在室外看到的光的类型。虽然太阳越过整个天空,他的光线在一天的不同时刻以不同角度照射这个世界,但是大多数的东西都是可见的,即使它们在阴影中。因为光线能从任何物体上反弹,最终照射到所有物体,所以不在太阳路径上的物体也会被照亮。这个角度来说上,甚至是房间里的灯泡也像表现得如同太阳一样传播环境光,因为如果房间不是很大的话,每一件物体都会被均等的照亮。环境光被模拟为没有原点,没有方向,并且对场景中的所有物体都有相同效果的光。
漫反射光强调了光线照射到物体表面的角度会影响物体被照射的亮度的事实。物体有光照射的一面比另一面(不直接面向光源的一面)更亮。我们只看到太阳散播的环境光没有明确的方向,然而,在太阳光中也有漫反射光。当他照射到一个高的建筑物的时候你通常能看到建筑物的一侧比另一侧亮一些。漫反射光的最重要的特性是他的方向。
镜面反射光相对于光本身更强调是物体的属性。当光线以特定的角度照射物体时,反射光使物体的某一部分闪光,而观察者站在在特定的的点才能看到。金属材质的物体经常会有一些镜面反射特性。例如,晴天的时候一辆汽车的边缘会闪闪发光。计算镜面反射光照不仅需要考虑光照射的方向(并且反射),也要考虑观察者的位置。
在3D应用程序中,你通常不需直接创建环境、漫反射、或镜面反射光。相反的,我们要使用光源,例如太阳(在室外)、一个灯泡(在室内)或者一个手电筒(在洞穴)。不同的光源类型有不同的环境光、漫反射光、镜面反射光强度组合和一些其他的特性。例如,手电筒有一个锥形的光束,距离较远的物体根本不会被照亮。
在以下的教程中我们将会开发一些实用的光源类型,同时学习基础光照模型。
我们将从一种叫做“平行光”的光源开始。平行光有方向但是没有具体的原点。这就意味着所有的光线彼此平行。光线的方向由一个向量确定,这个向量用来计算在场景中所有物体表面上的光,无论他们的位置在哪里。太阳光很好的符合了平行光的范畴。如果你尝试计算太阳光照射两个相邻的建筑的角度,你会得到两个几乎完全一样的值(即他们之间的不同仅仅是一个微小的分数)。这是因为太阳距离地球大概1.5亿千米远的地方,因此,我们简单地忽略它的位置而只需考虑它的方向。
平行光的另一个特性是无论与发光的物体距离多远,他的亮度都保持相同。这和我们在接下来的教程中学习的另一个光源点光源不同。点光源随着距离增大亮度变得越来越弱(例如灯泡)。
下面这张图片阐明了平行光:
我们已经看到在太阳中既有环境光也有漫反射光。我们在这一部分中讲述环境光,而在下一章中学习漫反射光。
在之前的教程中我们学习了如何从纹理中提取像素的颜色。颜色有三条通道(红、绿和蓝),每一个通道都是一个字节。这就意味着颜色取值范围是0到255.通道之间不同的组合可以产生不同的颜色。当所有的通道都为0时颜色为黑色。当他们都为255时颜色为白色。其他的颜色在两者之间。通过以相同的缩放因子同时缩放三个颜色通道的值,我们可以得到相同的颜色,只不过器明暗度不一样(取决于缩放因子)
当白光接触物体表面,反射的光仅仅是物体表面的颜色。反射光可能会亮或者暗一点,取决于光源的强度,不过还是相同的基色。如果光源是纯粹的红色(255,0,0),反射光只能是红色中的某一类型。这是因为纯红光没有绿色和蓝色通道可以从物体表面反射回来。如果物体表面是纯蓝色,则最终结果是纯黑色。基本原则是光线只能暴露出对象的实际颜色,而不能给它“上色”。
我们将光源的颜色指定为三个取值在【0-1】之间的浮点值。通过将光的颜色和对象的颜色相乘我们将得到反射的颜色。然而,我们也要考虑环境光的强度。因此,我们设定环境光的强度为一个范围在【0-1】之间的单独的浮点值,将它与我们刚刚计算得到所有通道的反射光相乘。下面的方程式总结了环境光的计算:
Pixel—像素 ambient intensity--环境光强度
在本次的教程代码实例中你可以通过按“A和“S”键来增加或者减小环境光的强度,观察在对在之前的教程中创建的纹理金字塔的影响。这只是平行光的环境光的部分,而其方向的因素还没有涉及。在下一章我们学习漫反射光时将会改变。现在你看到无论你在哪里看金字塔,它的亮度都相同。
很多人认为要尽量避免环境光,因为它看起来有些虚假,而且其简单的实现方式对于场景的真实性也没有太大的贡献。通过使用先进的方法例如全局照明,我们可以消除对环境光的需求,因为(使用全局照明)从物体反射出来而照射到其他对象的光一样可以被考虑。因为我们还没有那么深入,你通常需要一些少量的环境光,以避免物体的一面被照亮而另外一个面完全黑暗。天色渐晚的时候,为了使光看起来更好,我们需要进行大量的调整参数和协调工作。
Code Walkthru
随着时间的推移,我们的代码样本变得越来越复杂,而且这种趋势仍会持续。在本节教程中,除了实现环境光,我们也对代码做了大规模重构。我们将会把以后章节的代码放在一个更好的位置。主要改变如下:
1.将着色器的管理封装到Technique类中,包括编译和链接等操作。从现在开始我们将通过从Technique类派生的类实现视觉效果。
2.将GLUT初始化和callback管理移动到GLUTBacked组件中。这个组件注册自己来接收来自GLUT的callback调用并通过名为ICallbacks的C++接口把他们发送给应用。
3.将全局变量和main cpp文件中的变量移动到一个可被看做是“应用类”的类中。以后我们将把它扩展成一个为所有应用提供常用功能的基类。这种方法在许多的游戏引擎和框架中很受欢迎。
这个教程中的大多数的代码(除了光照中特定的代码)并不是新的,而仅仅是根据以上的设计原则的重新布局,因此只对新的头文件进行说明。
(glut_backend.h:24)
void GLUTBackendInit(int argc, char** argv);
bool GLUTBackendCreateWindow(unsigned intWidth, unsigned int Height, unsigned int bpp, bool isFullScreen, const char*pTitle);
GLUT很多特定的代码都被移动到一个叫做"GLUT backend"的组件,这个组件使我们能更容易的进行GLUT的初始化和使用上面的GLUTBackendCreateWindow函数创建窗口。
(glut_backend.h:28)
void GLUTBackendRun(ICallbacks* pCallbacks);
在初始化GLUT和创建窗口后,下一步是使用上面的封装函数执行GLUT 主循环。在这里新增加的ICallbacks接口对注册GLUT回调函数有所帮助。相对于让每一个application本身注册callbacks,GLUT backend组件注册自己的私有函数,并且将事件传送给上面函数调用所指定的对象。main application类通常会自行实现此接口,并在调用GLUTBackendRun时把自己作为一个参数传进去。
(technique.h:25) 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; };
在之前的教程中编译和链接着色器的所有的苦差事都是应用程序的一部分责任。technique类通过自身封装常用功能而使得派生类能集中精力于实现核心效果(又名“Technique”)。每一个technique首先必须通过调用Init()函数初始化。派生的technique必须调用基类的Init()(用来创建OpenGL程序对象),并且可以在这里添加自己私有的初始化。
在创建和初始化technique对象之后,接下来通常让派生technique类对所有需要的GLSL着色器(在字符数组中提供)调用受保护的函数AddShader()。最后,Finalize()用于链接对象。Enable()实际上是glUseProgram()的封装,所以无论何时切换technique和调用绘制函数时都要调用它。
这个类跟踪中间级的编译对象,并且在链接之后使用glDeleteShader()将他们删除。这有助于减少你的应用程序消耗的资源。为了实现更好的性能,OpenGL应用程序通常在加载时编译所有的着色器,而不是在运行的时候。通过在链接后立即移除对象,有助于你的应用保持OpenGL资源低消耗。程序对象本身使用glDeleteProgram()在析构函数中删除。
(tutorial17.cpp:49) 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; };
这是main应用类的架构,封装了我们已经熟悉的代码。Init()负责创建效果、加载纹理和创建顶点/索引缓存。Run()调用GLUTBackendRun()并且把对象本身作为一个参数传递。因为这个类实现了ICallbacks接口,所以所有的GULT事件以类中适当的方法结束。此外,所有以前文件全局部分定义的变量现在都是这个类的私有属性。
(lighting_technique.h:25)
struct DirectionalLight
{
Vector3f Color;
float AmbientIntensity;
};
这是平行光定义的开始。目前为止,只有环境光部分存在,而平行光本身还未涉及。当我们在下一个教程复习漫反射光时我们将会增加平行光。在这个结构中包括两个方面—颜色和环境光强度。颜色决定了对象的哪些颜色通道以何种强度可以被反射回来。例如,如果颜色是(1.0,
0.5, 0.0),那么之后对象的红色通道将会得到充分的反射,绿色通道将会被缩减到一半,蓝色通道将会被完全丢掉。这是因为一个对象只能反射入射光(光源不同——他们反射的光需要分开处理)。对于太阳光的颜色通常是纯粹的白色(1.0, 1.0, 1.0)。
AmbientIntensity指出了光的昏暗或明亮程度。强度1.0,你可以获得纯粹的白色,那样的话对象将被充分的照亮,或者定义为0.1,那样的话对象可见但是看起来非常的暗。
(lighting_technique.h:31) 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类的使用的第一个例子,LightingTechnique
是一个派生类,通过使用基类提供的编译和链接常用功能而实现照明。在创建对象后必须调用Init()函数,它仅仅调用Technique::AddShader()
和Techique::Finalize()来生成GLSL程序
(lighting.fs) #version 330 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; }
在本章中顶点着色器保持不变,它接着传递位置(在将它乘以WVP矩阵之后)和纹理坐标。所有的新的逻辑都进入片元着色器。在这里新增的内容是用 “struct” 关键字定义平行光。正如你看到的,这个关键字和在C/C++中的使用方法几乎一样。这个结构体和我们在应用程序代码中一样,而且我们必须这样,以使应用程序和着色器就可以交流。
现在有一个新的DirectionalLight类型的一致变量需要由应用更新。这个变量用于像素最终颜色的计算。和以前一样,我们对材质取样得到基本颜色。然后按上述的公式我们把它与颜色和环境光强度相乘。这总结了环境光的计算。
(lighting_technique.cpp:89)
m_WVPLocation = GetUniformLocation("gWVP");
m_samplerLocation = GetUniformLocation("gSampler");
m_dirLightColorLocation= =GetUniformLocation("gDirectionalLight.Color");
m_dirLightAmbientIntensityLocation= =GetUniformLocation("gDirectionalLight.AmbientIntensity");
为了在application中获取平行光一致变量,你必须独立得到他的两个组分的位置。LightingTechnique
类有4个GLuint位置变量,为了在顶点和片元着色器中获得一致变量。。WVP和sampler的位置以我们熟悉的方式获得。颜色和环境光强度用上面我们看到的方式获得——通过指定着色器中的一致变量的名字,后面跟随一个点,然后是在结构本身的组分的名字。设置这些变量的值的方式和设置其他变量一样。LightingTechnique
类提供了两种方法来设置平行光和WVP矩阵。第17章中的类在每次调用绘制命令前之前先调用他们以更新这些值。
这章你可以分别使用“a”和“s”键来增强和减弱环境光强度。按照在Tutorial17类中的KeyboardCB()函数来看是如何完成的。