OpenGL植物建模(附完整代码、注释清晰、分步讲解)

本文详细介绍了使用OpenGL进行三维建模的过程,包括初始化环境、加载纹理、开启光照和材质、封装显示列表以及实现键盘响应和摄像机移动。在myInit函数中加载纹理并设置光照和材质,通过显示列表封装绘制树干、花朵、连接部分和天空盒。myTree函数利用递归生成树的结构,通过键盘响应函数控制摄像机在世界坐标系中的移动,实现了观察视角的调整。
摘要由CSDN通过智能技术生成

  • 完整代码在我上传的资源处下载
  • 完整代码在我上传的资源处下载
  • 完整代码在我上传的资源处下载
  • 没有下载积分请把你qq私信发我
  • 没有下载积分请把你qq私信发我
  • 没有下载积分请把你qq私信发我

1、成果

在这里插入图片描述

2、myInit初始化函数

该函数中是一个初始化环境的函数,负责开启深度测试、自动法向、加载纹理数据、调用光照函数、材质设置以及调用生成列表函数。

void myInit() {
	glClearColor(1.0, 1.0, 1.0, 1.0);
	glDisable(GL_CULL_FACE);
	//自动法向防止变形
	glEnable(GL_NORMALIZE);
	glEnable(GL_DEPTH_TEST);
   //输入的深度值小于参考值,则通过
	glDepthFunc(GL_LESS);
	//纹理载入
    ……
	//打开纹理
	glEnable(GL_TEXTURE_2D);
	//调用打开光照函数
    ……
	//调用设置材质函数
	……
	//调用显示列表函数
	……
}

3、加载纹理数据

在myInit函数中设计for循环,依次图片路径数组读取图片,并依次加载进纹理数组中并分配编号,供后续贴图使用。每个编号均使用宏定义,更语义化。同时使用auxDIBImageLoad读取图片的宽高以及数据,不过VS2019中需要对图片路径字符转换为宽字符才能使用。

for (int i = 0; i < 9; i++) {
		//生成纹理编号
		glGenTextures(1, &ImagesIDs[i]);
		glBindTexture(GL_TEXTURE_2D, ImagesIDs[i]);
		WCHAR wfilename[256];
		memset(wfilename, 0, sizeof(wfilename));
		//该函数映射一个字符串到一个宽字符(unicode)的字符串
		MultiByteToWideChar(CP_ACP, 0, szFiles[i], strlen(szFiles[i]) + 1, wfilename, sizeof(wfilename) / sizeof(wfilename[0]));
		Images[i] = auxDIBImageLoad(wfilename);  //加载图片

		//Images[i] = auxDIBImageLoadA(szFiles[i]);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, Images[i]->sizeX, Images[i]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, Images[i]->data);

		//纹理被放大(一个纹元对应多个像素)
		//选择距像素中心最近的四个texel的加权平均值作为像素的颜色
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		//纹理被缩小(多个纹元对应一个像素)
		//选择距像素中心最近的四个texel的加权平均值作为像素的颜色
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

		//纹理第一维坐标大于1小于0时,表现为裁剪
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
		//纹理第二维坐标大于1小于0时,表现为裁剪
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

		//纹理环境
		//直接使用纹理图片替换
		glTexEnvf(GL_TEXTURE_2D, GL_TEXTURE_ENV_MODE, GL_REPLACE);
	}

4、打开光照和材质

设置光照和材质的函数都比较常规。在光照函数mySetupLights中,要设置环境光为白色,同时光源的位置中,最后一个参数为0,这样的光就是平行的光,更能模拟自然的太阳。

//灯光参数
void mySetupLights() {
	//环境光:白色的太阳光
	GLfloat ambientLight[] = { 1.0, 1.0, 1.0, 1.0 };
	//glColor3f(0.91f, 0.56f, 0.64f);
	//光源的颜色与物体的颜色值相乘(叉乘)
	GLfloat diffuseLight[] = { 0.9, 0.9, 0.9, 1.0 };  //散射光
	//镜面光:白色
	GLfloat specularLight[] = { 1.0, 1.0, 1.0, 1.0 };
	//阳光,模拟平行,第四个参数为0.0,定义相应的光源是定向光源,光线几乎是互相平行
	GLfloat posLight[] = { 0.0, 0.0, 0.0, 0.0 };
	GLfloat local_view[] = { 0.0 };

	//平滑模式
	glShadeModel(GL_SMOOTH);
	glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
	glLightfv(GL_LIGHT0, GL_SPECULAR, specularLight);
	glLightfv(GL_LIGHT0, GL_POSITION, posLight);
	glEnable(GL_LIGHT0);

	//镜面反射角度
	glLightModelfv(GL_LIGHT_MODEL_LOCAL_VIEWER, local_view);
}

//材质相关函数
void myMaterial() {
	glEnable(GL_COLOR_MATERIAL);

	//物体对各种光的强度
	GLfloat mat_ambient[] = { 1.0, 0.0, 0.0, 1.0 };
	GLfloat mat_diffuse[] = { 0.9, 0.38, 0.1, 1.0 };
	GLfloat mat_specular[] = { 1.0, 0.0, 0.0, 1.0 };
	GLfloat env_ambient[] = { 0.9, 0.38, 0.1, 1.0 };
	GLfloat mat_shinness[] = { 50.0 };

	glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
	glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
	glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
	glMaterialfv(GL_FRONT, GL_SHININESS, mat_shinness);

	//全局设置整个场景的环境光强度
	glLightModelfv(GL_LIGHT_MODEL_AMBIENT, env_ambient);
}

5、显示列表封装绘制函数

把绘制模型的函数,通过显示列表封装在myCreateList函数中,并提前在myInit函数中加载好,后续会在myTree函数中反复调用生成模型。每一个显示列表都用使用了glPushMatrix与glPopMatrix,这样所作变换操作就不会影响下一个变换,有些列表还使用了glPushAttrib与glPopAttrib,保证顶点处理时候的一些属性不影响后续顶点。

void myCreateList(void) {
	//创建树干绘制显示列表
	glNewList(TRUNK, GL_COMPILE);
	……
	//保护现场,不污染其他绘制
	glPushMatrix();
     ……
	glPopMatrix();
	//删除曲面
	glEndList();

	//创建花朵绘制显示列表,八个大部分
	glNewList(FLOWER, GL_COMPILE);
	glPushMatrix();
	//保护花朵的颜色等属性不污染其他绘制
	glPushAttrib(GL_ALL_ATTRIB_BITS);
	……
	glPopAttrib();
	glPopMatrix();
	glEndList();

	//花朵和树干连接
	glNewList(FLOWERANDTRUNK, GL_COMPILE);
	glPushMatrix();
	glPushAttrib(GL_LIGHTING_BIT);
……
	glPopAttrib();
	glPopMatrix();
	glEndList();

	//地板的显示列表
	glNewList(LAND, GL_COMPILE);
	glPushMatrix();
	……
	glPopMatrix();
	glEndList();

	//天空盒的显示列表
	glNewList(SKYBOX, GL_COMPILE); 
	glPushMatrix();
    ……
	glPopMatrix();
	glEndList();
}

5.1显示列表封装绘制函数:绘制树干

通过gluNewQuadric函数可以绘制出底大顶小的圆柱体,但是需要注意的是默认绘制的圆柱轴与世界坐标系中z轴是平行的,如图8中的Ⅰ,所以在树干进行旋转变换(所有的模型变换都是基于局部坐标系)glRotatef(-90, 1.0, 0.0, 0.0)。这个时候树干在坐标系中效果为图8-Ⅲ、图8-Ⅳ。
在这里插入图片描述图8 树干局部坐标系的变换

//创建树干绘制显示列表
	glNewList(TRUNK, GL_COMPILE);
	//二次曲面 
	cylquad = gluNewQuadric();
	//绑定纹理
	glBindTexture(GL_TEXTURE_2D, ImagesIDs[0]);
	//二次曲面绑定纹理
	gluQuadricTexture(cylquad, GL_TRUE);
	//保护现场
	glPushMatrix();
	//默认绘制的圆柱轴与z轴平行
	glRotatef(-90, 1.0, 0.0, 0.0);
	//绘制圆柱
	gluCylinder(cylquad, 0.1, 0.08, 1.7, 10, 10);
	glPopMatrix();
	//删除曲面
	gluDeleteQuadric(cylquad);
glEndList();

5.2显示列表封装绘制函数:绘制花朵

使用在线抠图网站,把模型描边(图9)扣出来。使用ppt,让坐标系图片与模型描边叠值(图10),人工读取花瓣特征坐标。为了贴图方便每一朵花瓣都切割为多个四边形,根据每片花瓣的长势,最少划分为2部分,最后划分为5部分。

//创建花朵绘制显示列表,八个大部分
	glNewList(FLOWER, GL_COMPILE);
	glPushMatrix();
	//保护花朵的颜色
	glPushAttrib(GL_ALL_ATTRIB_BITS);
	glBindTexture(GL_TEXTURE_2D, ImagesIDs[1]);
	//粉色
	glColor3f(0.91f, 0.56f, 0.64f);

	glBegin(GL_QUADS); //part1-1
	glTexCoord2f(0.0, 0.22); glVertex3f(0.0, 0.0, 0.0);
	glTexCoord2f(0.6, 0.044); glVertex3f(0.30, -0.09, 0.0);
	glTexCoord2f(0.8, 0.22); glVertex3f(0.44, 0.0, 0.0);
	glTexCoord2f(1.0, 1.0);	glVertex3f(0.46, 0.19, 0.0);
	glEnd();
   glBegin(GL_QUADS); //part1-2
    ……
	glEnd();

	glBegin(GL_QUADS); //part2-1
    ……
	glEnd();
    //省略了很多,详情见工程文件
    ……
    ……
	glBegin(GL_QUADS);//part8-5
    ……
 	glEnd();

	glPopAttrib();
	glPopMatrix();
	glEndList();

在这里插入图片描述
图11 绘制的花朵

5.3显示列表封装绘制函数:花与树干的连接

从这里开始就体现了glPushMatrix与glPopMatrix的作用。因为绘制树干与花朵都是各自的glPushMatrix与glPopMatrix中,则树干与花朵都是基于世界坐标系(0,0,0)建模。在封装花朵与树干的连接的时候,先绘制树干,然后进行glTranslatef(0.0, 1.7, 0.0)把花朵的局部坐标系向y轴平移1.7个单位,这个时候花朵才是在树干头顶开始绘制。坐标系如图12、13、14。

//花朵和树干连接
	glNewList(FLOWERANDTRUNK, GL_COMPILE);
	glPushMatrix();
	glPushAttrib(GL_LIGHTING_BIT);

	glCallList(TRUNK);
	glTranslatef(0.0, 1.7, 0.0);
	glPushMatrix();
	glRotatef(90, 0.0, 1.0, 0.0);
	glRotatef(50, 1.0, 0.0, 0.0);
	glCallList(FLOWER);
	glPopMatrix();
	
    glPushMatrix();
	glRotatef(180, 0.0, 1.0, 0.0);
	glRotatef(60, 1.0, 0.0, 0.0);
	glCallList(FLOWER);
	glPopMatrix();

	glPopAttrib();
	glPopMatrix();
   glEndList();

在这里插入图片描述图12花朵未平移图在这里插入图片描述图13 花朵平移后图

在这里插入图片描述图14 花朵平移坐标系示意图

5.4显示列表封装绘制函数:天空盒和地板

天空盒就是一个正六面体,然后按照顺序贴图。地板就是一块平行于世界坐标系的x0y面的正方形区域

//天空盒的显示列表
	glNewList(SKYBOX, GL_COMPILE); //天空背景
	glPushMatrix();
	glBindTexture(GL_TEXTURE_2D, ImagesIDs[3]);
	/** 绘制背面 */
	glBegin(GL_QUADS);
	/** 指定纹理坐标和顶点坐标 */
	glTexCoord2f(1.0f, 0.0f); glVertex3f(10, -10, -10);
	glTexCoord2f(1.0f, 1.0f); glVertex3f(10, 10, -10);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(-10, 10, -10);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(-10, -10, -10);
	glEnd();
	glPopMatrix();

	/** 绘制前面 */
	glBindTexture(GL_TEXTURE_2D, ImagesIDs[4]);
	glBegin(GL_QUADS);
	/** 指定纹理坐标和顶点坐标 */
	glTexCoord2f(1.0f, 0.0f); glVertex3f(10, -10, 10);
	glTexCoord2f(1.0f, 1.0f); glVertex3f(10, 10, 10);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(-10, 10, 10);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(-10, -10, 10);
	glEnd();

	/** 绘制底面 */
	glBindTexture(GL_TEXTURE_2D, ImagesIDs[5]);
	glBegin(GL_QUADS);
	/** 指定纹理坐标和顶点坐标 */
	glTexCoord2f(1.0f, 0.0f); glVertex3f(10, -10, 10);
	glTexCoord2f(1.0f, 1.0f); glVertex3f(10, -10, -10);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(-10, -10, -10);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(-10, -10, 10);
	glEnd();

	/** 绘制顶面 */
	glBindTexture(GL_TEXTURE_2D, ImagesIDs[6]);
	glBegin(GL_QUADS);
	/** 指定纹理坐标和顶点坐标 */
	glTexCoord2f(1.0f, 0.0f); glVertex3f(10, 10, 10);
	glTexCoord2f(1.0f, 1.0f); glVertex3f(10, 10, -10);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(-10, 10, -10);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(-10, 10, 10);
	glEnd();

	/** 绘制左面 */
	glBindTexture(GL_TEXTURE_2D, ImagesIDs[7]);
	glBegin(GL_QUADS);
	/** 指定纹理坐标和顶点坐标 */
	glTexCoord2f(1.0f, 1.0f); glVertex3f(-10, 10, -10);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(-10, 10, 10);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(-10, -10, 10);
	glTexCoord2f(1.0f, 0.0f); glVertex3f(-10, -10, -10);
	glEnd();

	///** 绘制右面 */
	glBindTexture(GL_TEXTURE_2D, ImagesIDs[8]);
	glBegin(GL_QUADS);
	/** 指定纹理坐标和顶点坐标 */
	glTexCoord2f(0.0f, 1.0f); glVertex3f(10, 10, -10);
	glTexCoord2f(1.0f, 1.0f); glVertex3f(10, 10, 10);
	glTexCoord2f(1.0f, 0.0f); glVertex3f(10, -10, 10);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(10, -10, -10);
	glEnd();

	glEndList();
	
}
	//地板的显示列表
	glNewList(LAND, GL_COMPILE);
	glPushMatrix();
	glBindTexture(GL_TEXTURE_2D, ImagesIDs[2]);
	glBegin(GL_QUADS);
	glTexCoord2d(0.0, 0.0);   glVertex3f(10, 0, 10);
	glTexCoord2d(1.0, 0.0);   glVertex3f(10, 0, -10);
	glTexCoord2d(1.0, 1.0);   glVertex3f(-10, 0, -10);
	glTexCoord2d(0.0, 1.0);   glVertex3f(-10, 0, 10);
	glEnd();
	glPopMatrix();
	glEndList()

6、投影变换

gluPerspective函数2个功能:一是产生一个视锥体让平面的图形开始有立体感,在世界坐标系中的z轴上平移时候有近大远小的变化。另外一个就是窗口发生变化时候,计算新的窗口的宽高比进行重绘视口,这样保证图形不变形。同时注意,变换之后,一定要变换回模型坐标系,以便正确接下来的模型绘制与变换。

//防止窗口变形函数
void myOnReshape(int w, int h) {
	WinWidth = w;
	WinHeight = h;
	glViewport(0, 0, w, h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(60.0, (GLfloat)w / (GLfloat)h, 1, 30.0);
	//恢复为模型变换的矩阵,当然也可以写在myDisplay中
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
}

7、封装树的绘制函数

树的绘制函数是本次实验中建模的关键。 采用递归思想,对函数进行封装,先写好只有一个树干,一朵花的函数(图13)。通过反复调用该函数生成若干个枝条,在正确进行拼接,最终得到一颗完整的树。三个要点:
 每段枝条都是生长在前一段枝条的末端
 每段枝条比前一条细
 花朵有大有小
if语句块决定递归的深度,以及正真的调用绘制。else语句块决定随机的模型变换矩阵,通过递归变换矩阵会随函数调用栈逐步相乘,最终起到逐步缩小枝条、缩小花朵、后一段枝条与前一段枝条的拼接的3大效果。

//生成树的函数
void myTree(int m) {
	long savedseed;
	//结束递归的标志
	//数字越大,枝条越细,也越多
	if (m == 8) {
		glPushMatrix();
		glRotatef(60.0 + myRand() * 120.0, 0.0, 1.0, 0.0);
		//正真的开始绘制树枝和连接处
		glCallList(FLOWERANDTRUNK);
		glPopMatrix();
	}else {
		//这里面只是决定递归的层数

		//调用树干
		glCallList(TRUNK);

		glPushMatrix();
		//把局部坐标系移到木桩的顶部开始
		glTranslatef(0.0, 1.7, 0.0);

		//绘制的花都很大,都缩小点
		glScalef(0.75, 0.75, 0.75);

		//生成随机数
		savedseed = rand();

		//递归调用
		glPushMatrix();
		//随机绕y轴旋转,表现形式就是屏幕平面上旋转(左升右下)
		glRotatef(10.0 + myRand() * 10.0, 0.0, 1.0, 0.0);

		//随机绕z轴旋转,表现形式往屏幕外面旋转
		glRotatef(20.0 + myRand() * 30.0, 0.0, 0.0, 1.0);

		//结束递归调用
		myTree(m + 1);
		glPopMatrix();

		//初始化新的随机数种子seed
		srand(savedseed);
		savedseed = rand();

		//被包裹在最开始的push与pop中,缩放效果的到叠加
		glPushMatrix();
		//随机绕y轴旋转,表现形式就是屏幕平面上旋转(左升右下)
		glRotatef(250.0 + myRand() * 80.0, 0.0, 1.0, 0.0);
		glRotatef(60.0 + myRand() * 30.0, 0.0, 0.0, 1.0);
		myTree(m + 1);
		glPopMatrix();

		//让随机数更随机一点
		srand(savedseed);
		savedseed = rand();

		glPushMatrix();
		glRotatef(180.0 + myRand() * 90.0, 0.0, 1.0, 0.0);
		glRotatef(30.0 + myRand() * 30.0, 0.0, 0.0, 1.0);
		myTree(m + 1);
		glPopMatrix();

		glPopMatrix();

	}
}

8、键盘响应函数&照相机的移动

通过绑定键盘按键和变量,控制照相机在世界坐标系中的位置,达到移动效果。完成上面代码后,运行编译,发现绘制的树太高了,超出屏幕上方看不见了,这个时候使用glPushMatrix与glPopMatrix对树与地板在他们的局部坐标系中往y轴与z轴方向平移。使的树与地板相对世界坐标系或者投影坐标系中往下与往后移动,得以看到树的全貌。

//myDisplay中调用绘制函数
void myDisplay(void) {

	//清除颜色缓冲和深度缓冲
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	//摄像机位置
	gluLookAt(g_LandR, g_UandD, g_Z, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

	//绘制树的时候,打开光照
	glEnable(GL_LIGHTING);

	//只想让树平移,而不想影响天空盒
	glPushMatrix();
	//相对世界坐标系的左右,前后,上下
	glTranslatef(0.0, -2.0, -2.0);
	//绘制树
	myTree(0);
	//绘制土地
	glCallList(LAND);
	glPopMatrix();

	glDisable(GL_LIGHTING);  //绘制天空背景的时候,关闭光照
	glCallList(SKYBOX);

	//交换缓冲
	glutSwapBuffers();
}

//键盘控制交互
void myKey(unsigned char key, int x, int y) {
	switch (key) {
	case 'w':
		g_UandD += 0.1;
		glutPostRedisplay();
		break;
	case 's':
		g_UandD -= 0.1;
		glutPostRedisplay();
		break;
	case 'a':
		g_LandR -= 0.1;
		glutPostRedisplay();
		break;
   case 'd':
		g_LandR += 0.1;
		glutPostRedisplay();
		break;
	case 'q':
		g_Z += 0.1;
		glutPostRedisplay();
		break;
	case 'e':
		g_Z -= 0.1;

		break;
	default:
		break;
	}

	//重要
	myOnReshape(WinWidth,WinHeight);
	glutPostRedisplay();
}

9、摄像机、视锥体、局部坐标系、世界坐标系的关系

所有代码完成后,并注释myDisplay函数中的这条glTranslatef语句后。所有代码之后造成的坐标系为图15。注意这个图是从计算机屏幕右侧面绘制的

//只想让树平移,而不想影响天空盒
	glPushMatrix();
	//相对世界坐标系的左右,前后,上下
	//glTranslatef(0.0, -2.0, -2.0);
	//绘制树
	myTree(0);
	//绘制土地
	glCallList(LAND);
	glPopMatrix();

在这里插入图片描述图15 建模后的坐标系与投影变换

  • 39
    点赞
  • 131
    收藏
    觉得还不错? 一键收藏
  • 39
    评论
评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值