在现代OpenGL+Qt学习笔记之六:绘制可旋转、带光照效果的三维物体中介绍了一个最简单的漫射光的原理及在OpenGL中的实现。本文将重点介绍一个更加经典的光照模型——Phong光照模型,也叫ADS(Ambient, Diffuse, Specular)光照模型。即在渲染物体时,不仅仅只考虑物体的漫反射(Diffuse),还考虑环境光(Ambient)和镜面反射(Specular)。同时还会介绍如何在GLSL中使用函数,示例程序将使用GLSL函数实现Phong光照。
Phong光照模型中,光强的计算是环境光强、漫反射光强和镜面反射光强的和。其中环境光照射到物体表面上的光强相同,且在各个方向上的反射光强也相同。因为环境光和入射方向和反射方向都无关,其光强( Ia 的计算只需要计算入射光强( La )和反射系数( Ka )的乘积即可。
漫反射光强的原理和计算方法在现代OpenGL+Qt学习笔记之六:绘制可旋转、带光照效果的三维物体中已经有详细介绍,这里仅给出它的计算公式:
镜面反射光能让物体表面产生高亮效果。镜面反射光的入射角和反射角相等,入射方向和反射方向关于法向对称(不考虑指向),如图所示:
和漫反射的情况不同,计算镜面反射的光强还需要考虑观察点的位置。即计算镜面反射光强需要如下4个(单位)向量:反射点到光源的方向s;反射方向r;反射点到观察点的方向v;法向量n。如下是各向量的示意图:
结合生活经验我们知道,当v和r方向重合时,反射光是最强的,而到v与r不重合,在夹角增大的过程中,反射光的光强会迅速衰减。该特点可以用v和r之间的余弦值刻画,但实际的衰减速度会比余弦值更高,因此在余弦值上增加了一个指数( f )。
其中 Ls 和 Ks 是和镜面反射相关的入射光强和反射系数,当 f 增大时,其反射光强随着r和v之间的夹角的增大而降低的速度更快。通常 f 的取值在1到200之前, f 越大,物体表面形成的镜面高光区域越小。
将所有的反射光强相加,即得到了最终的光照模型:
在介绍完GLSL中的函数之后,会通过一个程序介绍该模型在GLSL中使如何实现的。
当GLSL程序较大时,肯定得要将代码分成几个函数。下面是一个没有使用函数的例子:
// Structure declaration, to use as sample
struct Light
{
vec3 position;
vec3 diffuseColor;
vec3 attenuation;
};
// Shader entry point, just like in C, but no input params
void main()
{
vec3 myPosition = vec3(1.0, 0.0, 0.0);
// Let's create and initialize some ligths
Light light1 = Light(vec3(10.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0), vec3(1.0, 2.0, 3.0));
Light light2 = Light(vec3(0.0, 10.0, 0.0), vec3(1.0, 0.0, 0.0) ,vec3(1.0, 2.0, 3.0));
Light light3 = Light(vec3(0.0, 0.0, 10.0), vec3(1.0, 0.0, 0.0) ,vec3(1.0, 2.0, 3.0));
// Calculate simplified light contribution and add to final color
vec3 finalColor = vec3(0.0, 0.0, 0.0);
//distance is a GLSL built-in function
float distance1 = distance(myPosition, light1.position);
float attenuation1 = 1.0 / (light1.attenuation[0] + light1.attenuation[1] * distance1 + light1.attenuation[2] * distance1 * distance1);
finalColor += light1.diffuseColor * light1.attenuation;
// Let's calculate the same, for light2
float distance2 = distance(myPosition, light2.position);
float attenuation2 = 1.0 / (light2.attenuation[0] + light2.attenuation[1] * distance2 + light2.attenuation[2] * distance2 * distance2);
finalColor += light2.diffuseColor * light2.attenuation;
// Light 3
float distance3 = distance(myPosition, light3.position);
float attenuation3 = 1.0 / (light3.attenuation[0] + light3.attenuation[1] * distance3 + light3.attenuation[2] * distance3 * distance3);
finalColor += light3.diffuseColor * light3.attenuation;
// Now finalColor stores our desired color
}
将其改成用函数的方式实现:
struct Light
{
vec3 position;
vec3 diffuseColor;
vec3 attenuation;
};
vec3 CalculateContribution(const in Light light, const in vec3
position)
{
float distance = distance(position, light.position);
float attenuation = 1.0 / (light.attenuation[0] + light.attenuation[1] * distance + light.attenuation[2] * distance * distance);
return light.diffuseColor * light.attenuation;
}
// Shader entry point, just like in C, but no input params
void main()
{
vec3 myPosition = vec3(1.0, 0.0, 0.0);
Light light1 = Light(vec3(10.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0) ,vec3(1.0, 2.0, 3.0));
Light light2 = Light(vec3(0.0, 10.0, 0.0), vec3(1.0, 0.0, 0.0) ,vec3(1.0, 2.0, 3.0));
Light light3 = Light(vec3(0.0, 0.0, 10.0), vec3(1.0, 0.0, 0.0) ,vec3(1.0, 2.0, 3.0));
// Calculate light1
vec3 finalColor = CalculateContribution(light1, myPosition);
// Calculate light2
finalColor += CalculateContribution(light2, myPosition);
// Calculate light3
finalColor += CalculateContribution(light3, myPosition);
}
这样的代码相比于之前,更具有可读性,且易于调试、差错和修改。
在GLSL中,函数必须在其使用前声明,因此需要将函数的声明放在调用点之前。如上面的例子将函数声明和定义在了调用它的main函数之前,也可以通过先定义函数原型,然后在调用点之后定义函数主体。
GLSL函数和C/C++形式相同,可以不带返回值void,也可以带返回值,如这里的vec3。
对于上面的例子的函数原型:
vec3 CalculateContribution(const in Light light, const in vec3 position);
有两个输入变量,const关键字说明该变量的值不会在函数内部被改变;关键字in指定无论在函数内部对这个变量进行了怎样的操作,变量在函数调用结束后仍保持不变;关键字out是指变量仅仅会复制出函数,而不会复制进函数,即就算这个变量在输入前是有值得,但是其值不会被输入到调用的函数中;关键字inout指变量会被函数输入和输出,类似于C++中的引用和指针,其值会被赋值到函数中,也能被函数改变,然后输出。
out变量在输入函数前没有初始化
const关键字和out或inout一起是没有意义的,因此不要使用这种组合。
默认关键字的限定符是in,对于输入类型的参数,其值被输入到函数中,但是在其中发生改变,不会影响到其原始变量的值。
下面使用函数实现上面介绍的Phong光照模型,程序在现代OpenGL+Qt学习笔记之六:绘制可旋转、带光照效果的三维物体中的程序基础上增改。
首先是两个新的着色器function.frag:
#version 430
in vec3 LightIntensity;
layout( location = 0 ) out vec4 FragColor;
void main() {
FragColor = vec4(LightIntensity, 1.0);
}
和之前的片元着色器功能完全相同,接下来是相应的用函数实现的Phong光照模型的顶点着色器function.vert:
#version 430
layout (location = 0) in vec3 VertexPosition;
layout (location = 1) in vec3 VertexNormal;
out vec3 LightIntensity;
struct LightInfo {
vec4 Position; // Light position in eye coords.
vec3 La; // Ambient light intensity
vec3 Ld; // Diffuse light intensity
vec3 Ls; // Specular light intensity
};
uniform LightInfo Light;
struct MaterialInfo {
vec3 Ka; // Ambient reflectivity
vec3 Kd; // Diffuse reflectivity
vec3 Ks; // Specular reflectivity
float Shininess; // Specular shininess factor
};
uniform MaterialInfo Material;
uniform mat4 ModelViewMatrix;
uniform mat3 NormalMatrix;
uniform mat4 ProjectionMatrix;
uniform mat4 MVP;
void getEyeSpace( out vec3 norm, out vec4 position )
{
norm = normalize( NormalMatrix * VertexNormal);
position = ModelViewMatrix * vec4(VertexPosition,1.0);
}
vec3 phongModel( vec4 position, vec3 norm )
{
vec3 s = normalize(vec3(Light.Position - position));
vec3 v = normalize(-position.xyz);
vec3 r = reflect( -s, norm );
vec3 ambient = Light.La * Material.Ka;
float sDotN = max( dot(s,norm), 0.0 );
vec3 diffuse = Light.Ld * Material.Kd * sDotN;
vec3 spec = vec3(0.0);
if( sDotN > 0.0 )
spec = Light.Ls * Material.Ks *
pow( max( dot(r,v), 0.0 ), Material.Shininess );
return ambient + diffuse + spec;
}
void main()
{
vec3 eyeNorm;
vec4 eyePosition;
// Get the position and normal in eye space
getEyeSpace(eyeNorm, eyePosition);
// Evaluate the lighting equation.
LightIntensity = phongModel( eyePosition, eyeNorm );
gl_Position = MVP * vec4(VertexPosition,1.0);
}
接下来,主程序需要做一些简单改动,所有改动在initializeGL()函数中进行,首先将上述两个着色器添加到主程序的资源文件中,然后修改主程序中药用到的着色器源码文件的路径:
// vertex shader
QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
vshader->compileSourceFile(":/shader/function.vert");
// fragment shader
QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment, this);
fshader->compileSourceFile(":/shader/function.frag");
最后要再主程序中,依次为各个和光照及材质相关的系数赋值,将
// uniform light/material property
program->setUniformValue("Kd", QVector3D(0.9f, 0.5f, 0.3f));
program->setUniformValue("Ld", QVector3D(1.0f, 1.0f, 1.0f));
program->setUniformValue("LightPosition", view * QVector4D(0.0f,0.0f,5.0f,1.0f) );
改为
// uniform light/material property
QVector4D worldLight = QVector4D(5.0f,5.0f,2.0f,1.0f);
program->setUniformValue("Material.Kd", 0.9f, 0.5f, 0.3f);
program->setUniformValue("Light.Ld", 1.0f, 1.0f, 1.0f);
program->setUniformValue("Light.Position", view * worldLight );
program->setUniformValue("Material.Ka", 0.9f, 0.5f, 0.3f);
program->setUniformValue("Light.La", 0.4f, 0.4f, 0.4f);
program->setUniformValue("Material.Ks", 0.8f, 0.8f, 0.8f);
program->setUniformValue("Light.Ls", 1.0f, 1.0f, 1.0f);
program->setUniformValue("Material.Shininess", 100.0f);
程序运行结果如下:
本文主要介绍了ADS光照模型以及在GLSL中使用函数的一些知识,并通过一个程序演示GLSL函数的使用和ADS光照模型的实现。后续还会采用这种形式,整个几个知识点一起介绍和创建演示程序。
源码地址:http://download.csdn.net/download/chaojiwudixiaofeixia/9994280