OpenGL的基础光照和计算

1. 简介

OpenGL的光照是开启真实世界的一扇窗,由于光照的引入可以带来更大的真实场景模拟,更加强烈的视觉冲击。

2. 光的本质

我们日常说的光实际上是狭义上的可见光,广义上的光范围更广,包括了人眼不可见的红外、紫外等,光实际是一种电磁波,电磁波图如下所示
这里写图片描述

3. 颜色

说完了光,我们再说说颜色。现实世界中有无数种颜色,每个物体都有它自己的颜色。在计算机中我们只能通过数字模拟出有限种的颜色,尽管如此,对于人眼来说这种模拟已经足够了。因为数字世界模拟出的颜色种类已经远远超过人眼的辨识极限了。在数字世界中我们通过红、绿、蓝(也称为三原色),通过RGB的不同取值可以模拟出几乎所有的颜色。

我们经常说一个物体是什么颜色的,但是实际上物体真正并没有那种颜色,真实情况是物体反射的颜色。物体不接受的颜色成分恰恰是它所表现的颜色。如,太阳光被认为是由许多不同的颜色组合成的白色光。如果我们将白光照在一个蓝色的玩具上,这个蓝色的玩具会吸收白光中除了蓝色以外的所有颜色,不被吸收的蓝色光被反射到我们的眼中,使我们看到了一个蓝色的玩具。
下图显示的是一个珊瑚红的玩具,它以不同强度的方式反射了几种不同的颜色。
反射颜色
白色的阳光是一种所有可见颜色的集合,上面的物体吸收了其中的大部分颜色,它仅反射了那些代表这个物体颜色的部分,这些被反射颜色的组合就是我们感知到的颜色(此例中为珊瑚红)。

3.1 未开启光照的情形

在我们开始讲光照之前,先了解一下未开启光照的情况。默认情况下OpenGL并没有开启光照,当我们使用glColor函数设置绘制颜色的时候,我们实际上指定的是物体的最终渲染的颜色。示例程序如下:

//Legecy Code:
#pragma comment(lib, "glew32.lib")
#pragma comment(lib, "freeglut.lib")

#include <stdio.h>
#include <gl/glew.h>
#include <gl/glut.h>
#include <iostream>

int windowWidth = 0;
int windowHeight = 0;

bool leftMouseDown = false;
float mouseX, mouseY;
float cameraAngleX, cameraAngleY;
float xRot, yRot;

void drawCube()
{
    // Draw six quads
    glBegin(GL_QUADS);
    // Front Face
    // White
    glColor3ub((GLubyte)255, (GLubyte)255, (GLubyte)255);
    glVertex3f(1.0f, 1.0f, 1.0f);

    // Yellow
    glColor3ub((GLubyte)255, (GLubyte)255, (GLubyte)0);
    glVertex3f(1.0f, -1.0f, 1.0f);

    // Red
    glColor3ub((GLubyte)255, (GLubyte)0, (GLubyte)0);
    glVertex3f(-1.0f, -1.0f, 1.0f);

    // Magenta
    glColor3ub((GLubyte)255, (GLubyte)0, (GLubyte)255);
    glVertex3f(-1.0f, 1.0f, 1.0f);

    // Back Face
    // Cyan
    glColor3f(0.0f, 1.0f, 1.0f);
    glVertex3f(1.0f, 1.0f, -1.0f);

    // Green
    glColor3f(0.0f, 1.0f, 0.0f);
    glVertex3f(1.0f, -1.0f, -1.0f);

    // Black
    glColor3f(0.0f, 0.0f, 0.0f);
    glVertex3f(-1.0f, -1.0f, -1.0f);

    // Blue
    glColor3f(0.0f, 0.0f, 1.0f);
    glVertex3f(-1.0f, 1.0f, -1.0f);

    // Top Face
    // Cyan
    glColor3f(0.0f, 1.0f, 1.0f);
    glVertex3f(1.0f, 1.0f, -1.0f);

    // White
    glColor3f(1.0f, 1.0f, 1.0f);
    glVertex3f(1.0f, 1.0f, 1.0f);

    // Magenta
    glColor3f(1.0f, 0.0f, 1.0f);
    glVertex3f(-1.0f, 1.0f, 1.0f);

    // Blue
    glColor3f(0.0f, 0.0f, 1.0f);
    glVertex3f(-1.0f, 1.0f, -1.0f);

    // Bottom Face
    // Green
    glColor3f(0.0f, 1.0f, 0.0f);
    glVertex3f(1.0f, -1.0f, -1.0f);

    // Yellow
    glColor3f(1.0f, 1.0f, 0.0f);
    glVertex3f(1.0f, -1.0f, 1.0f);

    // Red
    glColor3f(1.0f, 0.0f, 0.0f);
    glVertex3f(-1.0f, -1.0f, 1.0f);

    // Black
    glColor3f(0.0f, 0.0f, 0.0f);
    glVertex3f(-1.0f, -1.0f, -1.0f);

    // Left face
    // White
    glColor3f(1.0f, 1.0f, 1.0f);
    glVertex3f(1.0f, 1.0f, 1.0f);

    // Cyan
    glColor3f(0.0f, 1.0f, 1.0f);
    glVertex3f(1.0f, 1.0f, -1.0f);

    // Green
    glColor3f(0.0f, 1.0f, 0.0f);
    glVertex3f(1.0f, -1.0f, -1.0f);

    // Yellow
    glColor3f(1.0f, 1.0f, 0.0f);
    glVertex3f(1.0f, -1.0f, 1.0f);

    // Right face
    // Magenta
    glColor3f(1.0f, 0.0f, 1.0f);
    glVertex3f(-1.0f, 1.0f, 1.0f);

    // Blue
    glColor3f(0.0f, 0.0f, 1.0f);
    glVertex3f(-1.0f, 1.0f, -1.0f);

    // Black
    glColor3f(0.0f, 0.0f, 0.0f);
    glVertex3f(-1.0f, -1.0f, -1.0f);

    // Red
    glColor3f(1.0f, 0.0f, 0.0f);
    glVertex3f(-1.0f, -1.0f, 1.0f);
    glEnd();
}


void SetupRC()
{
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glEnable(GL_DEPTH_TEST);
}

void RenderScene(void)
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glLoadIdentity();
    glTranslated(0, 0, -5);
    glRotated(cameraAngleY*0.5, 1, 0, 0);
    glRotated(cameraAngleX*0.5, 0, 1, 0);
    drawCube();

    glutSwapBuffers();
}


void ChangeSize(int w, int h)
{
    windowWidth = w;
    windowHeight = h;

    if (h == 0)
        h = 1;
    glViewport(0, 0, w, h);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0, w*1.0 / h, 0.01, 1000.0f);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}


void MouseFuncCB(int button, int state, int x, int y)
{
    mouseX = x;
    mouseY = y;

    if (button == GLUT_LEFT_BUTTON)
    {
        if (state == GLUT_DOWN)
        {
            leftMouseDown = true;
        }
        else if (state == GLUT_UP)
        {
            leftMouseDown = false;
        }
    }

}

void MouseMotionFuncCB(int x, int y)
{
    if (leftMouseDown)
    {
        cameraAngleX += (x - mouseX);
        cameraAngleY += (y - mouseY);

        mouseX = x;
        mouseY = y;
    }

    glutPostRedisplay();
}


int main(int argc, char* argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
    glutInitWindowSize(800, 600);
    glutCreateWindow("OpenGL");
    glutReshapeFunc(ChangeSize);
    glutDisplayFunc(RenderScene);
    glutMouseFunc(MouseFuncCB);
    glutMotionFunc(MouseMotionFuncCB);

    GLenum err = glewInit();
    if (GLEW_OK != err) {
        fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
        return 1;
    }

    SetupRC();

    glutMainLoop();
    return 0;
}

程序中通过使用glColor系列函数指定渲染最后呈现的颜色,运行结果如下:
ColorCube

我们通过指定每一个顶点的不同颜色,最终得到了上图所示的效果。现在有一个问题是:我们指定的只有顶点的颜色,那两个顶点间的颜色是怎么计算的呢?OpenGL通过设置着色模式来达到计算指定顶点间颜色的插值。设置函数

glShaderMode(GLenum     mode)
mode:设置模式,取值有GL_SMOOTH和GL_FLAT两种

SMOOTH插值使用的是一种线性的插值方式,如下图所示:
SMOOTH
vertex1和vertex2之间点的颜色是两者颜色的线性插值。
FLAT模式仅仅使用所绘制几何图元最后一点的颜色来给图元内部着色(Polygon是使用第一个点的颜色为内部着色)

3.2 开启光照的情形

如果仅仅使用不开启光照时候的情形,我们很难模拟真实的场景颜色。因为真实场景的颜色变幻莫测,不使用光照的模式使用的线性插值不可能模拟真实的场景。那么究竟应该怎么模拟呢?其实这是一个开放性的问题,每个人都可以有自己的模拟方式,都可以提出自己的算法(但是如果使用固定管线LegacyOpenGL,你难以实现自己所想,对于可编程的OpenGL,你可以大胆用自己的方式来实现光照的效果)。OpenGL有自己的一套模拟方式,称为Phong lighting model,这种模拟方式将光分成了三个成分:环境光(ambient)、散射光(diffuse)和镜面光(specular),如下图所示:
光照模型

3.2.1 三种光成分介绍
  1. 环境光:环境光不来自任何特定的方向,它来自某个光源,但是光线却是在场景中四处反射,没有方向可言。环境光照射的物体表面都是均匀照亮的。
  2. 散射光:散射光具有方向性,来自于一个特定的方向。它根据入射光线的角度在表面均匀地反射开来。因此,如果光线直接指向物体表面,它看上去更明亮一些。如果光线是从一个较大的角度照射到物体表面,那么它看上去显得暗一些。
  3. 镜面光:和散射光一样,镜面光也具有很强的方向性,但是它的反射角度很锐利,只有沿着一个特定的方向反射。高强度的镜面光倾向于在它照射物体的表面形成一个亮点。由于高度方向性的特点,能否被看到取决于观察者的位置。
3.2.2 物体的材质

在OpenGL中开启光照时,我们通过各种OpenGL的函数调用可以设定光的成分。当进行光照计算的时候,还需要指定物体的“颜色”。通过上面的介绍,读者可以知道物体的“颜色”是由它所反射的光来定义的。一个蓝色的球反射了光中大部分蓝色的光子,并吸收其他光子。如果照射光并不包含蓝色成分,那么这个蓝色的球在观察者眼中呈现黑色。
OpenGL光照计算的时候也是使用这么一种设定,我们并不把物体描述为具有一种特定的颜色,而是认为它由一些具有某种反射属性的材料所组成。在指定反射属性的时候,我们同样也指定这种材料对于环境光、散射光以及镜面光的反射属性。(正好和入射光的三种成分相对应)。

4. OpenGL光照API

下文我们分析OpenGL中怎么使用光照的API来得到光照的效果,分成两部分介绍。

4.1 Legecy OpenGL 光照

在Legecy OpenGL中光照计算需要通过设置光的成分以及物体的材质来完成,包括如下的API

光照计算函数描述
glEanble(GL_LIGHTING)开启光照计算(默认情况下OpenGL关闭光照计算)
glDisable(GL_LIGHTING)关闭光照计算
glLightModel设置光照模型
glLight设置光的参数
glMaterial设置物体材质
glColorMaterial设置物体材质
glNormal设置光照计算中物体表面的法线

上文中的代码演示了一个使用glColor指定颜色的立方体,并没有开启光照。当我们开启光照之后(只需要在SetupRC函数中添加一句代码glEnable(GL_LIGHTING);),可以发现场景完全变了,运行之后如下图所示:
WithoutLighting
这是由于当开启光照计算之后,glColor指定的颜色并没有任何作用。物体的颜色是通过计算光照计算来完成的。之所以会出现暗灰色,是因为当前场景中存在这一个默认的微弱的环境光,可以通过修改glLightModel修改它。

float globalAmbient[4] = { 0.5, 0.5, 0.5, 1.0 };
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, globalAmbient);

通过修改之后物体显得更亮了一些。当我们仅仅开启了光照计算并没有做任何设置的时候,OpenGL事实上使用了默认的设置。这个时候我们几何体的材质的环境光反射RGBA值是(0.2,0.2,0.2,1.0),由于不存在其他光的成分,因此其他的材质特性不起作用。(参与计算的是光的环境光成分和物体的环境光反射成分)

4.2 Core Profile中 光照的计算公式

4.2.1 环境光计算公式

环境光的计算相对简单,由于环境光对几何体的每个顶点的影响都是一样的。它仅仅将光源的环境光成分与材质的环境光成分相乘后叠加即可:计算的伪代码如下

光(R,G,B) x 材质(R',G',B') = (RxR', GxG', BxB')

4.2.2 散射光计算公式

当开启光照之后,指定几何体的散射材质,那么计算公式如下:
散射光 x 散射材质 = 散射的颜色
当散射光照射物体表面的时候,物体表面呈现的亮度与光线的入射角有很大的关系,如下图所示:
AoI
关于入射角的解释:
入射角
也就是说入射角为0时,物体表面最亮,入射角为90度是,物体表面最暗。
如果我们以一个值来定义亮度,0.0是最暗,1.0是最亮。那么可以简单的定义亮度由入射角的余弦值来表示。
入射角
计算两个角的余弦值只需要获得两个向量单位化之后进行点乘就可以了。
也就是说,为了计算散射光,我们需要获取两个向量
1. 顶点的法线向量
2. 光线照射到顶点的方向
在OpenGL中使用点的法线这种方式可能有些奇怪,因为按照数学上来说,点并没有发现,只有面才有法线。对于顶点的法线,一般来说我们需要提供一个顶点法线数组作为顶点的属性的输入数据。另外也可以通过计算面个法线,并把该法线作为所有组成面点的法线。(OpenGL中有时候为了做一些特殊的效果,常常会人为让一个平整表面的法线不一致,这些都不在本文讨论的范围之内,日后再表),法线坐标一般也是在物体的局部坐标下指定的。因为光线方向在相机坐标系下计算,因此法线也必须转换到相机坐标系下。法线的转换需要乘以法线转换矩阵(Normal Matrix),这个矩阵是模型视图矩阵的左上角3x3矩阵的逆的转置,具体的推导过程可以参考:OpenGL Normal Vector Transformation

另一个需要计算的是光源到顶点的方向,一般来说光源的位置是我们在程序中指定的,那么另一个位置是顶点的位置,顶点一般指定的是局部坐标下的位置(法线也是局部坐标下的法线方向)。在OpenGL固定管线中,当我们调用glLight设置光源的位置时,光源位置立刻被转换到相机坐标系中(使用模型视图矩阵),因此光照计算都是在相机坐标系中计算的。[在shader中,可以将这些计算过程放到世界坐标系中进行]。

现在我们可以计算散射成分了,它的计算公式可以表示如下:
DiffuseLight
其中N是顶点单位化的法线方向,L是光线方向,Cmat是几何体的材质中设置的散射成分,Cli是灯光中的散射光成分。

4.2.3 镜面光计算公式

镜面光让物体表面看起来像镜子一样明亮,当光线入射时,镜面反射的效果如下图:
Specular
当计算镜面光时,与计算散射光有点不同,它需要考虑观察者的位置(相机位置),当光线完全反射到相机中时,可以看到物体表面的亮点。
这里写图片描述

计算镜面光的过程如下:
1. 计算入射光方向
2. 计算反射光方向
3. 计算顶点到相机的方向
4. 计算反射光与顶点-相机方向的夹角
这个夹角基本上反映了镜面光能被看到的强度,因此如何处理最后的强度是一个需要讨论的问题。当角度很小是,我们看到的强度应该很大,当角度很大时,我们看到的强度应该很小。但是镜面光不同于散射光,镜面光的一致性很高,稍微角度大一点几乎完全看不到它。于是这个角度怎么设置才比较好呢?OpenGL使用了这样一种算法,通过计算两者夹角的余弦值,并为它设置一个指数项来模拟这种效果。这个指数项,是我们在固定管线中设置的shinness参数。它的效果需要根据情况自己调整。下图是设置不同的参数的一个初步的效果:
shinness
最终的算法如下所示:

//计算入射光方向
vec3 incidenceVector = -surfaceToLight; //a unit vector
//计算反射光方向(根据入射光方向和法线方向)
vec3 reflectionVector = reflect(incidenceVector, normal); //also a unit vector
//计算顶点到相机的方向
vec3 surfaceToCamera = normalize(cameraPosition - surfacePosition); //also a unit vector
//计算顶点-相机方向与反射光方向夹角的余弦值
float cosAngle = max(0.0, dot(surfaceToCamera, reflectionVector));
//引用镜面光的指数(shinness)
float specularCoefficient = pow(cosAngle, materialShininess);
//计算镜面光成分
vec3 specularComponent = specularCoefficient * materialSpecularColor * light.intensities;

4.2.4 光的衰减

在现实的光照中,当我们将光源远离物体表面时,物体表面看起来会更暗。一般来说光线的衰减和光源到物体表面的距离的二次方成反比,也就是:

i1d2

i是光照强度,d是光源到顶点的距离
当距离为0时,为了避免出现除数为0的情况,我们修改了一下分母
a=11+d2

a是衰减量,d是光源到顶点的距离
为了控制衰减的速度,再添加一个衰减的系数,最终公式变为:
a=11+kd2

4.2.5 平行光照

平行光可以被看作是光源在无穷远处的点光源,假设我们把这一特性应用于衰减的方程中,可以很容易知道物体表面应该越暗。这与现实情况不太相符(现实情况中我们假设太阳光是平行光,它事实上并没有根据它距地球的距离而有所衰减)于是OpenGL中在计算平行光的时候不考虑衰减。
和点光源不同,平行光并不需要一个位置,它只有一个方向。在齐次坐标中,为了表明它是一个方向,我们只需要设置它的第四维度是0就可以了。假设我们设定光源的位置在(1,0,0,0)处,也就是说光源在X轴正方向的无穷远处,于是平行光的方向是对该值取负,也就是(-1,0,0)说明光照方向是朝着X轴负方向。
计算的算法:只需要修改之前散射光和镜面光的光的方向向量即可

4.2.6 聚光灯

聚光灯和点光源类似,只是有一个不同点,聚光灯的光照被限制在一个圆锥形状内。聚光灯相比点光源多了两个变量,第一个是圆锥形的角度,第二个是圆锥中线的方向,如下图所示:
ConeDirection
计算的过程只需要得到聚光灯到顶点与圆锥中线方向的夹角小于ConeAngle,就可以使用点光源的计算方法,否则就让让衰减量最大。

// 1. 获取圆锥中心线的方向
vec3 coneDirection = normalize(light.coneDirection);

// 2. 获取顶点到聚光灯灯源的方向
vec3 rayDirection = -surfaceToLight;

// 3.计算二者的角度
float lightToSurfaceAngle = degrees(acos(dot(rayDirection, coneDirection)))

// 4. 判断是否在聚光灯照射下,如果超出,那么衰减最大
if(lightToSurfaceAngle > light.coneAngle){
  attenuation = 0.0;
}
  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值