现代OpenGL+Qt学习笔记之六:绘制带光照效果的三维物体
主要内容
本文仅考虑最简单的光照,即漫射光,同时在前面程序的基础上加入多模型的鼠标控制功能。此外,为了现实真正的三维渲染效果,本文将绘制的物体是一个如图所示的三维的圆环体。
漫射光
仅仅考虑漫射光是假设物体表面只会进行漫反射,即对于到达物体表面的入射光,其反射光的强度在各个方向上都是相同的,和观察者的位置无关。那么怎样计算漫射光强(也叫辐射量)呢?
如图,漫反射光强的计算只和两个向量有关:光到达曲面的法向
其中
当一定强度的入射光到达物体表面时,物体会对一部分的光进行吸收再反射,其反射的比例和物体本身的颜色有关,如一个纯红色的物仅会反射红光,吸收入射光中的绿色和蓝色成分。可以将该反射的比例设为
示例程序
下面是一个程序的示例,本章的程序是基于现代OpenGL+Qt学习笔记之四:使用Uniform变量实现对模型的旋转的,但是因为修改幅度比较大,就不做详细介绍。这里仅仅介绍几个关键点。
圆环体
首先在我们的程序中因为要用到光照,再绘制前面的平面三角形就无法显示真正的光照效果了,因此从本文开始,以后绘制的三维物体要稍微复杂一点,如圆环体。
这里主要定义了一个VBOTorus类,创建该类的对象,再在OpenGL部件类的渲染函数paintGL()中调用该类的render()函数即可。比较方便,还可以将模型数据和场景数据分开。有关该类的详细实现可以查看文后的源码。
#ifndef VBOTORUS_H
#define VBOTORUS_H
#include <QOpenGLFunctions>
#include <QOpenGLVertexArrayObject>
class QOpenGLVertexArrayObject;
class VBOTorus : protected QOpenGLFunctions
{
public:
VBOTorus(float outerRadius, float innerRadius, int nrings, int nsides);
~VBOTorus();
void render();
int getVertexArrayHandle();
protected:
QOpenGLVertexArrayObject vao;
private:
int faces, rings, sides;
void generateVerts(float * verts, float * norms, float * tex,
unsigned int * el,
float outerRadius, float innerRadius);
};
#endif // VBOTORUS_H
鼠标事件
为了从多个角度观察物体的光照效果,程序还将实现对模型的控制,包括旋转和缩放模型。实现该功能,主要是要再OpenGLWidget类中添加下列3个事件处理函数:
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void wheelEvent(QWheelEvent *event);
其中,在mousePressEvent()中我们要记录鼠标按下时的位置,而在mouseMoveEvent()中,我们要实现在鼠标左键按下且鼠标移动时,控制模型旋转,最后在wheelEvent()是在鼠标滚轮滚动时,实现物体的缩放(其实就是沿着z轴的平移,远小近大)。再添加和旋转、缩放相关的私有数据成员如下:
QMatrix4x4 model;
QMatrix4x4 view;
QMatrix4x4 projection;
GLfloat xtrans, ytrans, ztrans; // translation on x,y,z-axis
QVector2D mousePos;
QQuaternion rotation;
其中model, view, projection对应现代OpenGL+Qt学习笔记之五:OpenGL矩阵变换中介绍的模型矩阵,视图矩阵和投影矩阵。有关函数详细的实现和变量使用,还是要结合代码阅读了。这里只介绍一个比较重要的类QQuaternion。这是Qt提供的一个4元祖类,专门用来实现常用到的旋转操作。为什么是4元组呢?因为三维物体的旋转的确定包含旋转角度和旋转轴两个属性,而旋转轴具有3个分量,这就是为什么和旋转相关的类是4元组的原因。本示例代码中主要用到了该类的fromAxisAndAngle()函数,该函数是通过传递一个QVector3D类型的旋转轴和scalar类型的旋转角度计算得到和该旋转对应的4元组。
有关该控制过程的实现,可以参考文后源码。
光照计算
在OpenGL主程序提供了几何体的几何数据、光源光强和材料反射系数数据后,光照计算在顶点着色器或片元着色器中实现。
先看片元着色器:
#version 430
in vec3 LightIntensity;
layout( location = 0 ) out vec4 FragColor;
void main() {
FragColor = vec4(LightIntensity, 1.0);
}
很简单,就是通过顶点着色器输入一个反射光强(其实就是颜色了),再在最后添加alpha分量后输出即可。那么具体的模型旋转、缩放和光照计算是怎样的呢?
#version 430
layout (location = 0) in vec3 VertexPosition;
layout (location = 1) in vec3 VertexNormal;
out vec3 LightIntensity;
uniform vec4 LightPosition; // Light position in eye coords.
uniform vec3 Kd; // Diffuse reflectivity
uniform vec3 Ld; // Diffuse light intensity
uniform mat4 ModelViewMatrix;
uniform mat3 NormalMatrix;
uniform mat4 ProjectionMatrix;
uniform mat4 MVP;
void main()
{
vec3 tnorm = normalize( NormalMatrix * VertexNormal);
vec4 eyeCoords = ModelViewMatrix * vec4(VertexPosition,1.0);
vec3 s = normalize(vec3(LightPosition - eyeCoords));
LightIntensity = Ld * Kd * max( dot( s, tnorm ), 0.0 );
gl_Position = MVP * vec4(VertexPosition,1.0);
}
可以看到,输入信息必须包含顶点的位置和法向,其输出就是计算得到的漫发射光强。有关光源信息包含光源位置LightPosition、材料反射系数Kd和光源光强Ld。和旋转缩放变换相关的矩阵主要有模型视图矩阵ModelViewMatrix,法向矩阵NormalMatrix和投影矩阵ProjectionMatrix,有关这些矩阵的更详细信息参考现代OpenGL+Qt学习笔记之五:OpenGL矩阵变换。矩阵MVP是在客户端计算的projection*view*model得到的,这样做事为了减少重复计算,提高效率。
在main()函数中,首先要对法向和物体位置进行变换,然后计算点到光源的方向,注意
LightIntensity = Ld * Kd * max( dot( s, n ), 0.0 );
这是最终的漫反射光强的计算公式,对于维度相同的两个向量的相乘,GLSL自动实现了逐元素相乘,这里的余弦值的计算用的是max( dot( s, n ), 0.0 ),即取
小结
本文主要介绍了一种最简单的光照理论,以及其在现代OpenGL中的实现方式。同时为了从不同角度观察物体的光照效果,还实现了用鼠标控制物体的旋转和缩放。后面会介绍更加复杂一点的光照模型,使得渲染结果更加真实,还有逐片元渲染技术,可以令曲面表现更加平滑。
源码地址:http://download.csdn.net/download/chaojiwudixiaofeixia/9988568