OpenGL/GLUT实践:绘制旋转的立方体与雪人世界——添加光照与SOIL方式添加纹理
源码见GitHub:A-UESTCer-s-Code
1 运行效果
旋转的立方体实现效果:
雪人世界实现效果:
2 实现过程
2.1 几何转换
2.1.1 窗口刷新
利用透视变换实现窗口刷新:
- 通过透视投影来设置窗口刷新函数,使用
gluPerspective()
函数定义透视投影。
void ChangeSize(GLsizei w, GLsizei h)
{
GLfloat aspectRatio;
if (h == 0)
h = 1;
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
aspectRatio = (GLfloat)w / (GLfloat)h;
gluPerspective(60.0f, aspectRatio, 1.0, 400.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
2.1.2 绘制雪人场景
2.1.2.1 绘制雪人
- 绘制雪人身体部分:
- 首先设置颜色为白色。
- 使用
glTranslatef()
将当前矩阵沿着x、y和z轴移动到指定位置。 - 绘制一个半径为0.75的实心球体作为雪人的身体。
- 绘制雪人头部:
- 再次使用
glTranslatef()
将当前矩阵移动到头部位置。 - 绘制一个半径为0.25的实心球体作为雪人的头部。
- 再次使用
- 绘制雪人眼睛:
- 将当前矩阵保存(使用
glPushMatrix()
),以便后续绘制完成后恢复到初始状态。 - 设置眼睛颜色为黑色。
- 分别用
glTranslatef()
将当前矩阵移动到左眼和右眼的位置。 - 绘制半径为0.05的实心小球体作为眼睛。
- 恢复之前保存的矩阵状态(使用
glPopMatrix()
)。
- 将当前矩阵保存(使用
- 绘制雪人的鼻子:
- 设置鼻子颜色为橙红色。
- 使用
glRotatef()
将当前矩阵绕着x轴旋转0度(这里没有实际的旋转操作)。 - 绘制一个底半径为0.08、高度为0.5的圆锥体作为雪人的鼻子。
2.1.2.2 绘制场景
- 清除颜色和深度缓冲区:
- 使用
glClear()
函数清除颜色缓冲区和深度缓冲区,以便开始渲染新的帧。
- 使用
- 重置变换矩阵:
- 使用
glLoadIdentity()
函数重置变换矩阵,以确保每一帧的绘制都是从一个空白状态开始的。
- 使用
- 设置相机(镜头):
- 使用
gluLookAt()
函数设置相机的位置和方向。函数的参数为相机位置(x, 1.0f, z)
,相机目标位置(x+lx, 1.0f, z+lz)
,以及相机的上方向(0.0f, 1.0f, 0.0f)
。
- 使用
- 绘制地面:
- 使用白色绘制地面,通过
glColor3f()
设置颜色。 - 使用
glBegin()
和glEnd()
包裹的GL_QUADS
模式绘制一个矩形地面。
- 使用白色绘制地面,通过
- 绘制36个雪人:
- 使用两层嵌套的for循环,在不同的位置调用
drawSnowMan()
函数来绘制36个雪人。 - 内部的
glPushMatrix()
和glPopMatrix()
用于保存和恢复当前变换矩阵状态,以确保每个雪人的绘制都是相对独立的。
- 使用两层嵌套的for循环,在不同的位置调用
- 交换缓冲区:
- 使用
glutSwapBuffers()
交换前后缓冲区,以显示渲染好的图像。
- 使用
2.1.3 键盘事件
- 改变视线方向:
- 当用户按下左右箭头键时,会改变角度变量
angle
的值,从而改变视线的方向。 - 根据新的角度值重新计算视线向量的
lx
和lz
值,使用sin
和cos
函数将极坐标转换为平面坐标。
- 当用户按下左右箭头键时,会改变角度变量
- 改变镜头位置:
- 当用户按下上下箭头键时,会分别向前或向后移动镜头。
- 根据
lx
和lz
向量以及给定的粒度(fraction
)计算新的镜头位置(x, z)
,实现沿视线方向的移动。
2.1.4 运行效果
实现窗口刷新演示:
键盘控制前后移动和左右转头:
2.2 颜色
定义颜色方式:
OpenGL通过指定红、绿、蓝(RGB)成分的强度来定义颜色。
使用
glColor<x><t>(red, green, blue, alpha)
函数来设置颜色,其中:
<x>
表示参数的数量,可以是3(表示RGB颜色)或4(表示RGBA颜色,包括alpha通道);<t>
表示参数的数据类型。
着色模式(shading model):
- 着色模式定义了图元内部的颜色渲染方式。
- 默认情况下,OpenGL采用平滑着色模式(
GL_SMOOTH
)。当图元的顶点指定了不同的颜色时,OpenGL会在顶点之间进行平滑过渡,使得图元内部的颜色呈现渐变效果。 - 另一种着色模式是单调着色(
GL_FLAT
),在这种模式下,图元内部的颜色取决于最后一个顶点所指定的颜色。对于GL_POLYGON
图元,内部颜色取决于第一个顶点的颜色。
GL_SMOOTH
来选择平滑,实现效果如下:
GL_FLAT
单调着色模式,实现效果如下:
2.3 光照
2.3.1 绘制正方体
- 全局变量:
xrot
和yrot
:用于存储立方体绕x轴和y轴的旋转角度。xspeed
和yspeed
:用于控制立方体绕x轴和y轴的旋转速度。z
:用于控制立方体在z轴上的位置。
- changeSize函数:
- 设置OpenGL视口,并根据窗口大小设置透视投影。
- InitGL函数:
- 进行OpenGL的初始化设置,包括设置着色模式、清空颜色缓冲区和深度缓冲区等。
- renderScene函数:
- 清空颜色缓冲区和深度缓冲区。
- 重置模型视图矩阵,并移动相机位置到z轴为z的位置。
- 根据
xrot
和yrot
的值进行旋转。 - 绘制一个红色的立方体。
- 利用双缓冲机制交换前后缓冲区,将绘制的图像显示在屏幕上。
- 根据
xspeed
和yspeed
的值更新旋转角度。
- processSpecialKeys函数:
- 处理特殊键盘按键事件,包括上下左右箭头键和Page Up/Page Down键,分别用于控制立方体在z轴上的移动和绕x轴、y轴的旋转速度。
- 主函数:
- 初始化OpenGL和GLUT,并创建窗口。
- 注册回调函数,包括绘制函数、窗口大小变化函数和键盘特殊按键事件处理函数。
- 启用深度测试和双缓冲机制。
- 进入主循环,等待事件的发生。
实现效果:
2.3.2 添加光源
启用光源:
- 在键盘按下“l”键时,调用
glEnable(GL_LIGHTING);
来启用光照计算。
- 在键盘按下“l”键时,调用
设置光照模型:
设置光源参数:在程序头部设置了光源的参数,包括环境光和漫反射光的强度和位置。
ambientLight[]
:环境光的强度,用来模拟场景中各处的间接光照。diffuseLight[]
:漫反射光的强度,用来模拟光线直接照射到物体表面后的散射。position[]
:光源的位置,其中最后一个参数是1.0表示光源为定向光,0.0表示光源为点光源。
设置并启用光照:
在
InitGL
函数中,调用glLight()
函数来设置光源的参数,并启用光源GL_LIGHT0
。glLightfv
函数用于设置光源的各个属性,包括环境光、漫反射光、镜面反射光和光源位置等。
最终效果:
2.4 材质
2.4.1 方法一
使用 glMaterialfv
函数手动设置材质属性。
- 定义一个数组来指定物体表面的材质属性,例如
GLfloat gray[] = {0.9f, 0.0f, 0.0f, 1.0f};
表示物体表面反射90%的红光。 - 使用
glMaterialfv
函数设置材质属性,例如glMaterialfv(GL_FRONT, GL_DIFFUSE, gray);
用于设置散射光属性。
实现效果:
2.4.2 方法二
使用颜色追踪(Color Tracking)来设置材质属性。
- 调用
glColorMaterial
函数启用颜色追踪,例如glColorMaterial(GL_FRONT, GL_DIFFUSE);
表示追踪正面的散射光属性。 - 启用颜色追踪功能,使用
glEnable(GL_COLOR_MATERIAL);
。 - 使用
glColor
函数设置物体的颜色,例如glColor(0.0f, 0.0f, 0.9f, 1.0f);
表示设置物体为蓝色。
实现效果:
2.5 纹理
2.5.1 SOIL环境配置
首先在项目目录下创建
lib
、include
文件夹,分别将SOIL.lib
、SOIL.h
放入。在VS2022的项目中打开项目属性页,将如下两项加入刚刚创建的两个目录。
在链接器的常规中,加入lib目录。
在链接器的输入中,加入静态库的完整名称。
2.5.2 纹理加载
LoadGLTextures
函数:使用循环加载两张图片作为纹理,分别存储在
texture[0]
和texture[1]
中。调用
SOIL_load_OGL_texture
函数加载图片并将其转换为OpenGL纹理。该函数的参数包括图片路径、加载方式、生成新的纹理ID以及其他标志。检查纹理加载是否成功,如果失败则返回false。
对每张纹理进行绑定,并设置放大和缩小过滤器为线性过滤器(
GL_LINEAR
)。
renderScene
函数绘制立方体的各个面:
- 每个面都使用
glBegin(GL_QUADS)
开始绘制,并使用glEnd()
结束。 - 每个面的顶点坐标都使用
glVertex3f
指定。 - 每个顶点的纹理坐标都使用
glTexCoord2f
指定,以便纹理正确贴在立方体上。 - 每个面的法线(用于光照计算)都使用
glNormal3f
指定。
- 每个面都使用
2.6 雪人世界光照与材质
要在雪人世界加入光照与材质,我们只需要加入一个InitGL
函数,进行光照初始化;并加入普通按键控制,实现按l
时, 通过设置glDisable(GL_LIGHTING);
和glEnable(GL_LIGHTING);
,就可以打开/关闭光照。
同时,为了保持在光照下,颜色保持不变,我们只需要加入简单的两行代码使用glColorMaterial
、glEnable
函数,即可实现颜色追踪(Color Tracking)来设置材质属性。
int InitGL(GLvoid)
{
glColorMaterial(GL_FRONT, GL_DIFFUSE);
glEnable(GL_COLOR_MATERIAL);
GLfloat ambientLight[] = { 1.0f, 1.0f, 1.0f, 1.0f };
GLfloat diffuseLight[] = { 1.0f, 1.0f, 1.0f, 1.0f };
GLfloat position[] = { 0.0f, 0.0f, 2.0f, 1.0f };
glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
glLightfv(GL_LIGHT0, GL_POSITION, position);
glEnable(GL_LIGHT0);
return true;
}