[OpenGL] 动态的水面模拟


        真实的水面模拟在三维游戏领域一直是一个非常热门的问题,因为在大型的场景中,流体总是不可避免地会存在的。在这一方面,我也查了不少资料,总结而言,水面模拟一般有这么几个方法:


        绘制部分:


        1.比较简单的,就是贴图置换(或者是直着拖着一张贴图平移)(真的,确实有游戏是这么干的...)

        2.基于网格绘制,通过物理模拟(各种波动方程)来计算网格点位置,达到动态效果(本次demo的做法就是这样)

        3.使用动态的凹凸纹理映射(法线贴图),计算量相比物理模拟大大减少(看到了不少渲染图,都非常好看,可惜不会做贴图)

        4.使用粒子系统模拟水面


        渲染部分:

     

        1.反射和折射效果的实现

        2.高光(亮斑)的添加

        3.半透明

        4.……


        理论部分,参考的是《Mathematics for 3D Game Programming and Computer Graphics》一书中的实时流体模拟中的数学方法。最终只完成了绘制部分,还没有实现水面的渲染,因为渲染部分已经算的上是比模拟绘制更复杂的另一个大课题了。所以原作者给出的法线向量和切线向量就暂时被我舍弃了(因为它们是用在光照计算中的)


原理概述


        原文包含了大量的推导过程, 对于没有兴趣看的人,我总结一下全文表达的意思:

        1.水波的运动位移(z)满足偏微分方程:

        

        其中,c是波速,u是描述阻力大小的系数,x,y,z是空间坐标,t是时间。

       2.这个方程求解很麻烦,我们用近似的方法得到运动位移的公式:

        

        在这里,我们用三角形网格来表达水面,如下图,i,j代表点位置的索引,k是时间。

        


        3.为了保证迭代方程收敛,t,和c要满足:

       

       


        总而言之,就是一个公式(2)和两个约束条件(3),就算你看不懂推导,也可以直接使用结论了。


绘制三角形网格


        原理中有一张三角形网格图,首先,我们要把这个网格绘制出来。

        对于实体建模有所了解的人应该都知道,在计算机中,我们一般是用三维网格来表达各种物体的,具体而言,就是许多三角形面片。所以说,这里的网格和三维建模是统一的,它们的绘制方法也是一致的。当然我们可以直接用OpenGL中的顶点数组来完成这一切,但是在这里不引入过多其余的东西,所以直接调用了glBegin(GL_TRIANGLES)的方法来绘制三角形。

        我们需要输入的信息包括:

        1.每个顶点的坐标

        2. 每个顶点的法向量(在这里省略了)

        3.顶点的下标索引(描述一个三角形由哪几个顶点组成,只存索引,不存具体的顶点坐标)

        4.纹理坐标(描述纹理图片是如何映射到三角形上的)


        我们用一个数组indices来存放顶点下标索引,很显然,我们可以把三角形网格分成两部分,一组是直角在左下角的三角形集合,一组是直角在右上角的三角形集合。

        对于前者:

        

        我们从(0,0)顶点扫描到(width-2,height-2)顶点,每扫描到一个点(i,j),就把它所在三角形对应的三个点的索引放到索引数组。

        同理,对于后者:

        

        我们从(1,1)顶点扫描到(width -1 ,height -1)顶点,每扫描到一个点(i,j),就把它所在三角形对应的三个点的索引放到索引数组。


        注意初始的时候,要给某些点高度值,如果全是0的话,那么按迭代方程来看,所有的计算结果也都是0。然后我们直接利用原理中给出的公式重新计算每个点的波位移,最终可以得到这样的效果:


        



纹理映射


        其实到了这一步,我们的绘制已经算是完成了。我又加了点最简单的纹理映射,让它看起来稍微好一点。

        纹理映射和索引下标一样,也是由一个数组来维护,它包含了每个三角形中,三角形各个顶点与纹理图片上的点一一对应的关系。

        对于纹理图片而言,它的四个点的坐标是(0,0)(1,0)(0,1)(1,1)。由于我们希望的是把一整张纹理图直接盖到网格上,所以纹理映射中存的数据分布在[0,1]之间。

        假设网格为mxn规格,那么纹理图中单位长度width = 1/n,height = 1/m,同样的,我们按索引数组中的两组三角形来填充:

        


       三个顶点对应的纹理坐标:

        


       

          三个顶点对应的纹理坐标:

        


        实际效果中的过度变化比截图软件显示得更平滑。因为没有加入渲染(光照,阴影,反射,折射,透明度等等),所以看起来并不那么真实。缺陷在于网格之间的分界线过于明显,要解决这个问题,又是另外一个话题啦。


        


代码


fluid.h  (流体)
test.h  (纹理)
VectorClasses.h (顶点)
fluid.cpp    
texture.cpp
main.cpp

        请注意修改main中drawScene的count值来适应你计算机的速度,具体的参数也是可以调节的。


VectorClasses.h 可以在这里拿到,运行之后会报错未定义标识符Sqrt之类的……把它改成math.h里的sqrt就好啦


fluid.h

#pragma once
#include "VectorClasses.h"  


class Fluid
{
private:

	long            width; //宽
	long            height; //高

	Vector3D        *buffer[2]; //缓冲区
	long            renderBuffer;  //当前渲染的缓冲区

	//Vector3D        *normal; //法线
	//Vector3D        *tangent; //切线

	int             *indices[3]; //索引
	float       *texcoords[3][2]; //纹理坐标
	float           k1, k2, k3; //多项式系数

	int texture;
public:

	//n:网格宽 m:网格高 d:网格点之间的距离
	//t:时间 c:波速 mu:阻力系数

	Fluid(long n, long m, float d, float t, float c, float mu,int tex);
	~Fluid();

	void Evaluate(void);
	void draw();
};

test.h

#pragma once    
#define GLUT_DISABLE_ATEXIT_HACK      
#include "GL/GLUT.H"      
void loadTex(int i, char *filename, GLuint* texture);

fluid.cpp(注释部分为切线和法线的计算,用于渲染)

#include<stdlib.h>
#include"fluid.h"
#include"test.h"
Fluid::Fluid(long n, long m, float d, float t, float c, float mu,int tex)
{

	texture = tex;
	width = n; //宽度
	height = m;  //高度
	long count = n * m;  //网格点个数

	buffer[0] = new Vector3D[count];  //缓冲区1
	buffer[1] = new Vector3D[count];  //缓冲区2
	renderBuffer = 0;  //渲染缓冲区

	//normal = new Vector3D[count]; //法线
	//tangent = new Vector3D[count]; //切线


	for (int i = 0; i < 3; i++) {
		indices[i] = new int[2 * (n - 1)*(m - 1)];
	}
	for (int i = 0; i < 6; i++) {
		texcoords[i % 3][i / 3] = new float[2 * (n - 1)*(m - 1)];
	}
	
	// Precompute constants for Equation (15.25).  
	//预先计算:流体表面方程的多项式系数
	float f1 = c * c * t * t / (d * d);
	float f2 = 1.0F / (mu * t + 2);
	k1 = (4.0F - 8.0F * f1) * f2;
	k2 = (mu * t - 2) * f2;
	k3 = 2.0F * f1 * f2;

	// Initialize buffers.  
	//初始化缓冲区
	long a = 0;

	for (long j = 0; j < m; j++)
	{
		float y = d * j;
		for (long i = 0; i < n; i++)
		{
			if(i==0||j==0||i==n-1||j==m-1)buffer[0][a].Set(d * i, y, 0.0F);
			else {
				int r = rand() % 2;
				if (r == 0)buffer[0][a].Set(d * i, y, 1.0F);
				else buffer[0][a].Set(d * i, y, 0.0F);
			}
			buffer[1][a] = buffer[0][a];
		//	normal[a].Set(0.0F, 0.0F, 2.0F * d);
		//	tangent[a].Set(2.0F * d, 0.0F, 0.0F);
			a++;
		}
	}
	
	a = 0;
	float w = 1.0f / width;
	float h = 1.0f / height;
	for (int i = 0; i < n - 1; i++) {
		for (int j = 0; j < m - 1; j++ ) {
			long t = i*n + j;
			indices[0][a] = t;
			indices[1][a] = t + 1;
			indices[2][a] = t + n;

			texcoords[0][0][a] = i*w;
			texcoords[0][1][a] = j*h;

			texcoords[1][0][a] = (i + 1)*w;
			texcoords[1][1][a] = j*h;

			texcoords[2][0][a] = i*w;
			texcoords[2][1][a] = (j + 1)*h;

			a++;
		}
	}
	
	for (int i = 1; i < n; i++) {
		for (int j = 1; j < m; j++) {
			long t = i*n + j;
			indices[0][a] = t;
			indices[1][a] = t - 1;
			indices[2][a] = t - n;

			texcoords[0][0][a] = i*w;
			texcoords[0][1][a] = j*h;

			texcoords[1][0][a] = (i - 1)*w;
			texcoords[1][1][a] = j*h;

			texcoords[2][0][a] = i*w;
			texcoords[2][1][a] = (j - 1)*h;

			a++;
		}
	}
}

Fluid::~Fluid()
{
	//delete[] tangent;
	//delete[] normal;
	delete[] buffer[1];
	delete[] buffer[0];
}

void Fluid::Evaluate(void)
{
	// Apply Equation (15.25).  
	//调用流体表面方程
	for (long j = 1; j < height - 1; j++)
	{
		//当前顶点位移
		const Vector3D *crnt = buffer[renderBuffer] + j * width;
		//前一顶点位移
		Vector3D *prev = buffer[1 - renderBuffer] + j * width;

		// z(i,j,k+1) = k1 * z(i,j,k) + k2 * z(i,j,k-1) +
		//     k3 * (z(i+1,j,k) + z(i-1,j,k) + z(i,j+1,k) + z(i,j-1,k)

		for (long i = 1; i < width - 1; i++)
		{
			prev[i].z = k1 * crnt[i].z + k2 * prev[i].z +
				k3 * (crnt[i + 1].z + crnt[i - 1].z +
					crnt[i + width].z + crnt[i - width].z);
		}
	}

	// Swap buffers.
	//交换缓冲区
	renderBuffer = 1 - renderBuffer;

	// Calculate normals and tangents.  
	//计算法线和切线
	/*
	for (long j = 1; j < height - 1; j++)
	{
		const Vector3D *next = buffer[renderBuffer] + j * width;
		Vector3D *nrml = normal + j * width;
		Vector3D *tang = tangent + j * width;

		for (long i = 1; i < width - 1; i++)
		{
			nrml[i].x = next[i - 1].z - next[i + 1].z;
			nrml[i].y = next[i - width].z - next[i + width].z;
			tang[i].z = next[i + 1].z - next[i - 1].z;
		}
	}
	*/
}

void Fluid::draw()
{
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, texture);  //选择纹理texture[status]     
	glBegin(GL_TRIANGLES);

	for (int i = 0; i < 2*(height-1)*(width-1); i++) {	
	/*	glNormal3f(normal[(int)indices[i].x].x,
			normal[(int)indices[i].x].y,
			normal[(int)indices[i].x].z);*/
		glTexCoord2f(texcoords[0][0][i],texcoords[0][1][i]);
		glVertex3f(buffer[renderBuffer][indices[0][i]].x,
			buffer[renderBuffer][indices[0][i]].y,
			buffer[renderBuffer][indices[0][i]].z);
	/*	glNormal3f(normal[(int)indices[i].y].x,
			normal[(int)indices[i].y].y,
			normal[(int)indices[i].y].z);*/
		glTexCoord2f(texcoords[1][0][i], texcoords[1][1][i]);
		glVertex3f(buffer[renderBuffer][indices[1][i]].x,
			buffer[renderBuffer][indices[1][i]].y,
			buffer[renderBuffer][indices[1][i]].z);
	/*	glNormal3f(normal[(int)indices[i].z].x,
			normal[(int)indices[i].z].y,
			normal[(int)indices[i].z].z);*/
		glTexCoord2f(texcoords[2][0][i], texcoords[2][1][i]);
		glVertex3f(buffer[renderBuffer][indices[2][i]].x,
			buffer[renderBuffer][indices[2][i]].y,
			buffer[renderBuffer][indices[2][i]].z);
	}

	glEnd();
	glDisable(GL_TEXTURE_2D);
}

texture.cpp

#define _CRT_SECURE_NO_WARNINGS    
#include<stdio.h>    
#include<windows.h>    
#include"test.h"    
#define BITMAP_ID 0x4D42     


//读纹理图片      
static unsigned char *LoadBitmapFile(char *filename, BITMAPINFOHEADER *bitmapInfoHeader)
{

	FILE *filePtr;    // 文件指针      
	BITMAPFILEHEADER bitmapFileHeader;    // bitmap文件头      
	unsigned char    *bitmapImage;        // bitmap图像数据      
	int    imageIdx = 0;        // 图像位置索引      
	unsigned char    tempRGB;    // 交换变量      

								 // 以“二进制+读”模式打开文件filename       
	filePtr = fopen(filename, "rb");
	if (filePtr == NULL) {
		printf("file not open\n");
		return NULL;
	}
	// 读入bitmap文件图      
	fread(&bitmapFileHeader, sizeof(BITMAPFILEHEADER), 1, filePtr);
	// 验证是否为bitmap文件      
	if (bitmapFileHeader.bfType != BITMAP_ID) {
		fprintf(stderr, "Error in LoadBitmapFile: the file is not a bitmap file\n");
		return NULL;
	}
	// 读入bitmap信息头      
	fread(bitmapInfoHeader, sizeof(BITMAPINFOHEADER), 1, filePtr);
	// 将文件指针移至bitmap数据      
	fseek(filePtr, bitmapFileHeader.bfOffBits, SEEK_SET);
	// 为装载图像数据创建足够的内存      
	bitmapImage = new unsigned char[bitmapInfoHeader->biSizeImage];
	// 验证内存是否创建成功      
	if (!bitmapImage) {
		fprintf(stderr, "Error in LoadBitmapFile: memory error\n");
		return NULL;
	}

	// 读入bitmap图像数据      
	fread(bitmapImage, 1, bitmapInfoHeader->biSizeImage, filePtr);
	// 确认读入成功      
	if (bitmapImage == NULL) {
		fprintf(stderr, "Error in LoadBitmapFile: memory error\n");
		return NULL;
	}
	//由于bitmap中保存的格式是BGR,下面交换R和B的值,得到RGB格式      
	for (imageIdx = 0; imageIdx < bitmapInfoHeader->biSizeImage; imageIdx += 3) {
		tempRGB = bitmapImage[imageIdx];
		bitmapImage[imageIdx] = bitmapImage[imageIdx + 2];
		bitmapImage[imageIdx + 2] = tempRGB;
	}
	// 关闭bitmap图像文件     
	fclose(filePtr);
	return bitmapImage;
}

//加载纹理的函数      
void loadTex(int i, char *filename, GLuint* texture)
{

	BITMAPINFOHEADER bitmapInfoHeader;                                 // bitmap信息头      
	unsigned char*   bitmapData;                                       // 纹理数据      

	bitmapData = LoadBitmapFile(filename, &bitmapInfoHeader);

	glBindTexture(GL_TEXTURE_2D, texture[i]);
	// 指定当前纹理的放大/缩小过滤方式      
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

	glTexImage2D(GL_TEXTURE_2D,
		0,         //mipmap层次(通常为,表示最上层)       
		GL_RGB,    //我们希望该纹理有红、绿、蓝数据      
		bitmapInfoHeader.biWidth, //纹理宽带,必须是n,若有边框+2       
		bitmapInfoHeader.biHeight, //纹理高度,必须是n,若有边框+2       
		0, //边框(0=无边框, 1=有边框)       
		GL_RGB,    //bitmap数据的格式      
		GL_UNSIGNED_BYTE, //每个颜色数据的类型      
		bitmapData);    //bitmap数据指针      

}

main.cpp

#define _CRT_SECURE_NO_WARNINGS      

#include <stdio.h>      
#include <string.h>      
#include<time.h>    
#include <stdlib.h>    
#include"test.h"      
#include"fluid.h"

//纹理缓冲区  
GLuint texture[13];

//视区      
float whRatio;
int wHeight = 0;
int wWidth = 0;
//流体
Fluid *f;

//视点      
float center[] = {27, 10, 0 };
float eye[] = { 27,-50, 50 };


void drawScene()
{
	static int count = 0;
	count++;
	if (count > 1000) {
		count = 0;
		f->Evaluate();
	}
	f->draw();
}

void updateView(int height, int width)
{
	glViewport(0, 0, width, height);
	glMatrixMode(GL_PROJECTION);//设置矩阵模式为投影       
	glLoadIdentity();   //初始化矩阵为单位矩阵          
	whRatio = (GLfloat)width / (GLfloat)height;  //设置显示比例     
	gluPerspective(45.0f, whRatio, 1.0f, 150.0f); //透视投影        
												  //glFrustum(-3, 3, -3, 3, 3,100);      
	glMatrixMode(GL_MODELVIEW);  //设置矩阵模式为模型    
}

void reshape(int width, int height)
{
	if (height == 0)      //如果高度为0          
	{
		height = 1;   //让高度为1(避免出现分母为0的现象)          
	}

	wHeight = height;
	wWidth = width;

	updateView(wHeight, wWidth); //更新视角          
}


void idle()
{
	glutPostRedisplay();
}


void init()
{
	srand(unsigned(time(NULL)));
	glEnable(GL_DEPTH_TEST);//开启深度测试       

	glEnable(GL_LIGHTING);  //开启光照模式       

//	Fluid(long n, long m, float d, float t, float c, float mu);
	
	glGenTextures(1, texture);
	loadTex(0, "water.bmp", texture);
	/*
	   n = 10
	   m = 10
	   d = 1
	   t = 1
	   c = 0.5
	   u = 0
	*/
	f = new Fluid(30, 30, 2, 1, 0.2, 0, texture[0]);
}

void redraw()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);//清除颜色和深度缓存        
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();   //初始化矩阵为单位矩阵          
	gluLookAt(eye[0], eye[1], eye[2], center[0], center[1], center[2], 0, 1, 0);                // 场景(0,0,0),Y轴向上      
	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

	glShadeModel(GL_SMOOTH);
	glFrontFace(GL_CCW);
	glEnable(GL_CULL_FACE);
	// 启用光照计算    
	glEnable(GL_LIGHTING);
	// 指定环境光强度(RGBA)    
	GLfloat ambientLight[] = { 2.0f, 2.0f, 2.0f, 1.0f };

	// 设置光照模型,将ambientLight所指定的RGBA强度值应用到环境光    
	glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambientLight);
	// 启用颜色追踪    
	glEnable(GL_COLOR_MATERIAL);
	// 设置多边形正面的环境光和散射光材料属性,追踪glColor    
	glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);

	drawScene();//绘制场景       
	glutSwapBuffers();//交换缓冲区      
}

int main(int argc, char *argv[])
{

	glutInit(&argc, argv);//对glut的初始化             
	glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE);
	//初始化显示模式:RGB颜色模型,深度测试,双缓冲               
	glutInitWindowSize(500, 500);//设置窗口大小             
	int windowHandle = glutCreateWindow("Simple GLUT App");//设置窗口标题               
	glutDisplayFunc(redraw); //注册绘制回调函数             
	glutReshapeFunc(reshape);   //注册重绘回调函数             

	glutIdleFunc(idle);//注册全局回调函数:空闲时调用           

	init();
	glutMainLoop();  // glut事件处理循环           
	return 0;
}



water.bmp
  • 23
    点赞
  • 97
    收藏
    觉得还不错? 一键收藏
  • 31
    评论
在使用VSCode和OpenGL模拟地球公转之前,您需要确保已经安装好了OpenGL库和相关的开发环境。下面是一个简单的示例代码,可以帮助您开始实现地球公转的模拟: ```cpp #include <GL/glut.h> GLfloat angle = 0.0f; // 地球旋转角度 void renderScene() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0f, 1.0f, 0.0f); glRotatef(angle, 0.0f, 1.0f, 0.0f); // 绕Y轴旋转 // 绘制地球 glColor3f(0.0f, 0.0f, 1.0f); // 蓝色 glutSolidSphere(1.0f, 50, 50); glutSwapBuffers(); } void update(int value) { angle += 1.0f; // 更新旋转角度 if (angle > 360) { angle -= 360; } glutPostRedisplay(); // 请求重绘 glutTimerFunc(25, update, 0); // 设置定时器,每25毫秒更新一次 } int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); glutInitWindowSize(800, 600); glutCreateWindow("Earth Rotation"); glEnable(GL_DEPTH_TEST); glutDisplayFunc(renderScene); glutTimerFunc(25, update, 0); glutMainLoop(); return 0; } ``` 请确保已安装OpenGL库,并在VSCode中配置好相应的编译环境。建议使用CMake构建项目并进行编译链接。 该示例代码使用GLUT库来创建窗口和处理输入,以及OpenGL函数来绘制地球和进行旋转。在`renderScene`函数中,我们使用`glRotatef`函数实现地球的旋转。在`update`函数中,我们更新旋转角度并请求重绘。 希望这个示例能帮助您开始在VSCode中使用OpenGL模拟地球公转。如有其他问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值