计算机图形学大作业
参考[图形学] 实时流体模拟中的数学方法一文,二维波方程可以表示为:
设有一个矩形区域(水面),定义
本项目中不考虑阻力系数,并将待绘制水面看做一个个小三角面元,如下图
区域的长度(横向)为 this->length,宽度(纵向)为 this->width, 然后设置两个缓冲区(buffer[0]、buffer[1]),每个缓冲区分别保存当前时刻和上一时刻的所有顶点坐标,大小为 (this->width + 1) * (this->length + 1)
代码部分如下:
//初始化波动方程参数
float f1 = speed * speed * time * time / (size * size);
this->k1 = 2.0f - 4.0f * f1;
this->k2 = - 1.0f;
this->k3 = f1;
//根据此刻和上一时刻的数据求出下一次刷新所有定点的位置
for (long i = 1; i < this->width; i++)
{
Point3D* now = buffer[render] + i * this->length; //now为当前时刻顶点信息,pre为上一时刻
Point3D* pre = buffer[1 - render] + i * this->length;
for (long j = 1; j < this->length; j++)
{
pre[j].z = k1 * now[j].z + k2 * pre[j].z + //使用波动方程计算下一时刻坐标信息
k3 * (now[j + 1].z + now[j - 1].z + (now + 1)[j].z
+ (now - 1)[j].z);
}
}
//交换缓冲区
render = 1 - render;在主程序中,以上计算将每隔一段时间进行一次,以达到动态效果,运行结果如下图:
这一部分主要将纹理贴图应用到水面的网格上, 其中读取bmp图片的函数(teture.h -> LoadBmpFile)是自定义的,也可以直接调用其他库中的函数, opengl加载纹理主要用到下面几个函数:
glEnable(GL_TEXTURE_2D); //启动纹理
glGenTextures(1, texture); //加载一个纹理对象到texture
glBindTexture(GL_TEXTURE_2D, texture); //创建或使用一个已命名的纹理,这里是绑定一个2D纹理到texture
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //设置纹理的一些特性,比如这里设置纹理过滤方式为线性插值
void WINAPI glTexImage2D( //指定二维纹理图像,即将图像信息绑定到该纹理上,此处纹理对应的是上面最近的绑定纹理
GLenum target,
GLint level,
GLint internalformat,
GLsizei width,
GLsizei height,
GLint border,
GLint format,
GLenum type,
const GLvoid *pixels
);
glTexCoord2f(GLfloat s,GLfloat t); //后面跟glVertex3f, 将纹理图上的坐标到网格上的坐标做一个映射
glDisable(GL_TEXTURE_2D); //关闭纹理代码部分主要实现将纹理图正确映射到网格区域中,在Fluid类中设置 indices[3][faceNum] 和 texcoords[3][2][faceNum],前者保存从面到点的索引,后者存放 从网格到纹理图片的坐标映射,算法部分如下:
long idx = 0;
float wUnit = 1.0f / this->width; //纹理图片的大小固定为1×1
float lUnit = 1.0f / this->length;
for (int i = 0; i < this->width; i++) {
for (int j = 0; j < this->length; j++, idx++) {
//下三角面片
long t = i * (this->length + 1) + j;
indices[0][idx] = t;
indices[1][idx] = t + 1;
indices[2][idx] = t + this->length + 1;
texcoords[0][0][idx] = j * lUnit;
texcoords[0][1][idx] = i * wUnit;
texcoords[1][0][idx] = (j + 1) * lUnit;
texcoords[1][1][idx] = i * wUnit;
texcoords[2][0][idx] = j * lUnit;
texcoords[2][1][idx] = (i + 1) * wUnit;
}
}
for (int i = 1; i < this->width + 1; i++) {
for (int j = 1; j < this->length + 1; j++, idx++) {
//上三角面片
long t = i * (this->length + 1) + j;
indices[0][idx] = t;
indices[1][idx] = t - 1;
indices[2][idx] = t - this->length - 1;
texcoords[0][0][idx] = j * lUnit;
texcoords[0][1][idx] = i * wUnit;
texcoords[1][0][idx] = (j - 1) * lUnit;
texcoords[1][1][idx] = i * wUnit;
texcoords[2][0][idx] = j * lUnit;
texcoords[2][1][idx] = (i - 1) * wUnit;
}
}texcoords下标对应三角面元的位置如下图:
增加纹理后的效果如下:
然后调节波动方程的参数以美化绘制结果,并在水面四周添加五个背景纹理,做成虚拟的天空盒, 同时改写键盘回调函数,能通过按键控制视角的移动,这里主要使用glLookAt函数,需要注意每个参数的效果:
//a和d控制视角左右移动,w和s控制视角上下移动,1和2控制上下高度
void keyBoard(unsigned char key, int x, int y) {
switch (key)
{
case 'a':
case 'A':
angleXy += 2.0; break;
case 'd':
case 'D':
angleXy -= 2.0; break;
case 'w':
case 'W':
angleXz += 2.0; break;
case 's':
case 'S':
angleXz -= 2.0; break;
case '1':
z += (float)sin(PI * angleXz / 180.0f) * 1.0;
break;
case '2':
z -= (float)sin(PI * angleXz / 180.0f) * 1.0;
}
glutPostRedisplay();
}
gluLookAt(x, y, z, float(x + 100 * cos(PI * angleXz / 180.0f)),
float(y + 100 * sin(PI * angleXy / 180.0f)),
float(z + 100 * sin(PI * angleXz / 180.0f)), 0.0f, 0.0f, 1.0f);效果如下:
(这里可以看到水面还是有明显的边界感,因为此前的版本纹理映射算法有些小bug,在之后版本已经调整好了)
在场景中添加光源,包括环境光、漫射光、镜面反射和光源,还有水面的材质设置,代码包括以下:
GLfloat ambient[] = { 2.0f, 2.0f, 2.0f, 1.0f }; // 整个场景的环境光强度
GLfloat diffuse[] = { 1.0f, 1.0f, 1.0f, 1.0f };
GLfloat specular[] = { 1.0f, 1.0f, 1.0f, 1.0f }; //漫射光
GLfloat position[] = { 100.0f, 180.0f, 180.0f, 1.0f }; //光源位置
GLfloat mat_specular[] = { 1.0f, 1.0f, 1.0f, 1.0f }; //镜面反射参数
GLfloat mat_shininess[] = { 50.0 }; //高光指数
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular); //设置材质
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
glLightfv(GL_LIGHT0, GL_POSITION, position);
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
glLightfv(GL_LIGHT0, GL_SPECULAR, specular);
glEnable(GL_LIGHTING); //启用光源
glEnable(GL_LIGHT0);
glEnable(GL_DEPTH_TEST);其中,反射光需要得到顶点的法向量,法向量的计算较为复杂,先通过计算得到每个面元的法向量,如果以面法向量作为顶点法向量, 得到的反射效果如下:
即类似镜片的效果,然后计算顶点法向量。如果一个顶点在多个面中,则需要取相关的所有面的法向量之和再规范化为单位向量,这部分需要讨论边界和内部节点,代码如下:
void computeNormal() {
//计算所有三角面元的法向量
long idx = 0;
for (int i = 0; i < this->width; i++) {
for (int j = 0; j < this->length; j++, idx++) {
long t = i * (this->length + 1) + j;
int i_0 = t;
int i_1 = t + 1;
int i_2 = t + this->length + 1;
Vector3D z_o(buffer[0][i_1].x - buffer[0][i_0].x, buffer[0][i_1].y - buffer[0][i_0].y, buffer[0][i_1].z - buffer[0][i_0].z);
Vector3D z_t(buffer[0][i_2].x - buffer[0][i_0].x, buffer[0][i_2].y - buffer[0][i_0].y, buffer[0][i_2].z - buffer[0][i_0].z);
faceNormal[idx].cross(z_o, z_t).normalize();
}
}
for (int i = 1; i < this->width + 1 ; i++) {
for (int j = 1; j < this->length + 1; j++, idx++) {
long t = i * (this->length + 1) + j;
int i_0 = t;
int i_1 = t + 1;
int i_2 = t + this->length + 1;
Vector3D z_o(buffer[0][i_1].x - buffer[0][i_0].x, buffer[0][i_1].y - buffer[0][i_0].y, buffer[0][i_1].z - buffer[0][i_0].z);
Vector3D z_t(buffer[0][i_2].x - buffer[0][i_0].x, buffer[0][i_2].y - buffer[0][i_0].y, buffer[0][i_2].z - buffer[0][i_0].z);
faceNormal[idx].cross(z_o, z_t).normalize();
}
}
//计算所有顶点的法向量
long triangles = this->width * this->length;
idx = 0;
for (int i = 0; i < this->width + 1; i++) {
for (int j = 0; j < this->length + 1; j++, idx++) {
if (i == 0) {
//第一行
if (j == 0) {
vertexNormal[idx] = faceNormal[0];
}
else if (j == this->length) {
vertexNormal[idx] = (faceNormal[idx - 1] + faceNormal[triangles + idx - 1]).normalize();
}
else {
vertexNormal[idx] = (faceNormal[idx - 1] + faceNormal[triangles + idx - 1] +
faceNormal[idx]).normalize();
}
}
else if (i < this->width) {
//中间行
long base = this->length;
if (j == 0) {
vertexNormal[idx] = (faceNormal[i * base] + faceNormal[(i - 1) * base] +
faceNormal[(i - 1) * base + triangles]).normalize();
}
else if (j == this->length) {
vertexNormal[idx] = (faceNormal[(i + 1) * base - 1] + faceNormal[(i + 1) * base + triangles - 1] +
faceNormal[i * base + triangles - 1]).normalize();
}
else {
vertexNormal[idx] = (faceNormal[i * base + j - 1] + faceNormal[i * base + j + triangles - 1] +
faceNormal[i * base + j] + faceNormal[(i - 1) * base + j + triangles - 1] +
faceNormal[(i - 1) * base + j] + faceNormal[(i - 1) * base + j + triangles]).normalize();
}
}
else {
//最后面一行
long base = this->length;
if (j == 0) {
vertexNormal[idx] = (faceNormal[(i - 1) * base] + faceNormal[(i - 1) * base + 1]).normalize();
}
else if (j == this->length) {
vertexNormal[idx] = faceNormal[i * base - 1];
}
else {
vertexNormal[idx] = (faceNormal[(i - 1) * base + j + triangles - 1] + faceNormal[(i - 1) * base + j] +
faceNormal[(i - 1) * base + j + triangles]).normalize();
}
}
}
}
}之后经过测试,发现光照效果不明显,不知道是法向量计算错误还是全局光照设置有问题,请各位自行判断。
本项目测试时用的环境为Visual Studio 2019,用到的OpenGL库为"GL/GLUT.H",具体环境搭建可以参考 此篇文章,注意安装Nuengl管理。 运行需要包含的头文件在"头文件"文件夹中,纹理贴图都放在了"资源文件"文件夹中,主程序为main.cpp,将其导入一个空项目中即可编译运行。







