在3D真实感图形学中,光照是很重要的技术。从物理上讲,一束光是由很多细小的粒子“光子”组成,这些光子在空气中传输,在物体的表面折射,反射,最终进入我的视觉系统,形成了我们眼中看到的真实世界。本文将主要讨论OpenGL 中的几种光照模型。

最基本的光照模型称作 环境光/漫反射光/高光(Ambient/Diffuse/Specular)光照模型。 环境光就是在没有太阳的情况下,你周围的光照情况,它模拟的是太阳光照射地面后,在不同的位置被反射,折射,最后混合在一起的一种光线,环境光没有方向,没有起点,它是一种均匀的光照效果,即使在阴影中,也有环境光存在。 漫反射光是光照在物体表面被反射,最终进入我们眼睛的效果,漫反射光和光线的方向,以及物体表面的法线有关,比如一个物体有2面,当漫反射光只照在一个面时,这个面是亮的,而另一个面是暗的。 高光是物体本身的一种属性,当光作用于物体时,物体上某个部分可能特别亮,这就是高光,高光会随着我们的视点移动而不断改变位置。金属物体一般都有这种高光属性,计算高光必须考虑光线方向,光照物体的法向,以及视线的方向。

首先,我们需要知道如何在一个纹理上采样像素颜色。像素颜色有3个通道,红绿蓝(RGB),每个通道都是一个单字节,也就是说每个通道的颜色值范围是[0,255],不同的RGB值代表不同的颜色,(0,0,0)表示黑色,(255,255,255)表示白色。我们可以对一个颜色RGB值按比例缩放,可以看到颜色不变,但亮度越来越低。

一个物理反射的光的颜色与物体的颜色和入射光的颜色有关。例如,红色的物理只能反射红色的光,蓝色的光照射到红色物体上,没有光被反射,而白色的光照射到红色的物体上,仅仅反射白光中的红色通道部分。下面的公式可以很好地计算环境光:

Ambient Lighting

环境光和漫反射光最大的区别在于:漫反射光依赖于光源的方向,而环境光和光源方向完全无关,环境光在场景中是均匀分布的,对场景中的所有物体都有效,而漫反射光在物体朝向光源的一面才有光照效果,在背面则没有光照效果。

漫反射光照模型实际上基于Lambert’s cosine law ,也就是说物体表面反射的光照强度和与光源方向和物体表面法向有关, 光源方向和法向的夹角的越小,则物体表面反射的光强越大。 用公式表示就是光源的强度乘以光源和物体表面法向夹角的余弦值。

如果物体表面是个平面,那么法向就是一个固定的向量,但真实物体表面往往并不是平面,所以物体表面的法向是不断变化的。,我们通常都是对每个顶点定义法向,而三角形面光栅化后的每个像素,它的法向是由顶点法向插值得到。这种光照模型称作 Phong Shading。

为了计算高光,我们会再次引入一个新的参数视点位置,因为高光会随着视点的移动而改变位置。在一些角度,高光看起来会更亮。金属物体通常都有高光的效果。如下图:

Specular Lighting

图中涉及到的参数的解释:

  • ‘I’ 是照亮物体表面的入射光
  • ‘N’ 是物体表面法向
  • ‘R’ 是入射光在照射到物体表面后的发射光,反射光和入射光是沿着法线对称的。
  • ‘V’ 是物体表面上的点指向视点的向量。
  • ‘alpha’ ‘R’ 和 ‘V’两个向量的夹角

角度’alpha’为0的时候,R和V重合,此时高光的强度最大,当观察者视线逐渐离开的时候,’alpha’逐渐变大,高光效果会逐渐变小。 基于这个依据,我们将用点积操作来求得’alpha’的cosine值,这将作为我们计算高光时的因子,当’alpha’大于90度时候,cosine值为负值,此时没有高光效果。

为了计算’alpha’,我们需要两个向量 ‘R’ 和 ‘V’。 ‘V’向量可以用摄像机(视点)位置减光照作用的顶点位置得到(注意这2个位置应该都是位于世界坐标系)。在GLSL中,有个内置的函数’reflect’就是用来计算反射向量的,函数的两个参数分别为入射光方向和平面的法向向量。

我们在方向光的前提下,研究了基本的光照模型(环境光,漫反射光,高光)。方向光没有起点,所有光线都是沿着一个方向,它的强度不会随着距离的增加有任何变化。 与普通的方向光不同的是,点光源是起始于一个点,向四面照射,会随着传输距离增加而衰减,所以在点光源属性中,我们会增加一个光源位置。

通常点光源衰减程度离物体距离的平方成反比,但在距离比较小的时候,上面的公式在3D图形学中,效果并不好,所以我们计算点光源衰减系数时候, 常使用下面的公式:它包括3个因子,常量因子,线性因子和二次因子:

Point Light Attenuation

计算点光源时需要的步骤:

  1. 环境光的计算和方向光一样
  2. 在世界坐标系中,计算点(像素)到光源的方向,做为光源的方向向量。
  3. 计算像素到光源的距离,用来计算衰减因子。
  4. 把环境光、漫反射光和高光加起来,然后乘以衰减因子。

光照模型的OpenGL实现代码

纹理(Texture)

使用SOIL库来载入图片,并创建纹理:

static GLuint texture, textureTarget = GL_TEXTURE_2D;

static void CreateTextureBuffer() {
    // generate texture object.
    glGenTextures(1, &texture);
    glBindTexture(textureTarget, texture);

    // The general function glTexParameterf control many aspects of the texture sampling operation.
    // Here we specify the filter to be used for magnification and minification.
    glTexParameterf(textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // load texture image.
    texture = SOIL_load_OGL_texture( // load an image file directly as a new OpenGL texture
        textureImageName,
        SOIL_LOAD_AUTO,
        SOIL_CREATE_NEW_ID,
        SOIL_FLAG_MIPMAPS | SOIL_FLAG_INVERT_Y | SOIL_FLAG_NTSC_SAFE_RGB | SOIL_FLAG_COMPRESS_TO_DXT
    );
}

另一种加载图片会创建纹理的方法:

// another way to load texture image with low-level API of SOIL:
int img_width, img_height;
unsigned char* img = SOIL_load_image("texture.png", &img_width, &img_height, NULL, 0);
glTexImage2D(textureTarget, 0, GL_RGBA, img_width, img_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img);

对比两种做法,显然直接使用SOIL_load_OGL_texture函数更加方便。

给模型贴纹理时,我们需要通过UV坐标来告诉OpenGL用哪块图像填充三角形。每个顶点除了位置坐标外还有两个浮点数坐标:U和V。这两个坐标用于访问纹理。

TextureVertex Vertices[4] = {
    TextureVertex(vec3f(-1.0f, -1.0f, 0.5773f) , vec2f(0.0f, 0.0f)),
    TextureVertex(vec3f(0.0f, -1.0f, -1.15475f), vec2f(0.5f, 0.0f)),
    TextureVertex(vec3f(1.0f, -1.0f, 0.5773f)  , vec2f(1.0f, 0.0f)),
    TextureVertex(vec3f(0.0f, 1.0f, 0.0f)      , vec2f(0.5f, 1.0f))
};

同时,在绘图循环函数中,需要绑定纹理的定点属性供Shader使用:

glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(TextureVertex), 0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(TextureVertex), (const GLvoid *)sizeof(vec3f));
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);
// make sure it active, and then bind again.
glActiveTexture(textureUnit);
glBindTexture(textureTarget, texture);
glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_INT, 0);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);

在Frame Shader中,使用纹理值给模型着色:

gl_FragColor = texture2D(gSampler, TexCoord.xy)

使用了纹理,我们注意到一个问题:三维立体图形的背面一侧也会显示出来,显然,这是不合理的。有一种思路是让GPU检查摄像机与三角形前后位置关系。如果摄像机在三角形前面则显示该三角形;如果摄像机在三角形后面,就不显示该三角形。我们可以使用如下代码来实现背面剔除

// enable back face culling, a common optimization used to drop triangles before the heavy process
// of rasterization. just make it look butter.
// see more @ http://ogldev.atspace.co.uk/www/tutorial16/tutorial16.html
glFrontFace(GL_CW);
glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);

同时,这个优化也有助于减少GPU需要渲染的三角形的数量,提升渲染的效率。