第一个 OpenGL 程序:旋转的立方体(VS2022 / MFC)

文章目录

  • OpenGL API
  • 开发环境
  • 在 MFC 中使用 OpenGL
    • 初始化 OpenGL
    • 绘制图形
    • 重置视口大小
  • 创建 MFC 对话框项目
  • 添加 OpenGL 头文件和库文件
  • 初始化 OpenGL
  • 画一个正方形
    • OpenGL 坐标系
    • 改变默认颜色
  • 重置视口大小
  • 绘制立方体
  • 使用箭头按键旋转立方体
  • 深度测试
  • 添加纹理
    • 应用纹理
    • 换一个纹理
  • 自动旋转
  • 销毁资源
  • 更进一步
  • 参考

转载请注明出处 https://blog.csdn.net/blackwoodcliff/article/details/132282723

OpenGL API

OpenGL 有两套 API:立即渲染模式(Immediate mode,也就是固定渲染管线,也称为兼容模式)和 核心模式(Core-profile)。
其实这与 GPU 的发展历史有关,最初的 GPU 是不能编程的,也叫固定管线,就是把数据按照固定的通路走完,后来发展出了可编程的 GPU,也叫可编程管线,一开始只能用汇编写 GPU 程序,然后进一步发展出了 GPU 高级编程语言,也就是现在所说的着色语言(Shading Language)。
了解了 GPU 的发展历史,我们自然就明白为什么 OpenGL 会有两套 API 了。立即渲染模式 就是最初 GPU 不能编程时的 API,核心模式 则是使用了着色语言的现代 API。
最新版本的 OpenGL 对立即渲染模式也是支持的,故而也把立即渲染模式称为兼容模式
核心模式 更灵活,效率更高,在当前实际应用中,已经很少有人使用立即渲染模式了。不过立即渲染模式虽然古老低效,但也更简单,作为了解 OpenGL 的基本概念,快速入门,还是很有用的。
本文作为入门教程,为降低学习门槛,因此使用更简单的 立即渲染模式

开发环境

目前网上的 OpenGL 教程大多会使用 GLFW 和 glad 这两个库。GLFW 是一个跨平台的窗口管理库,glad 是一个 OpenGL 函数加载库。
本文为了简单起见,不打算花费精力配置开发环境,所以不会使用 GLFW 和 glad 这两个库。
Windows 内置了对 OpenGL 1.1 的支持,如果使用 兼容模式,完全可以使用 Windows 内置的 OpenGL 1.1 来开发,这样可以省去配置开发环境的工作,聚焦于 OpenGL 本身。

在 MFC 中使用 OpenGL

在开始之前,先了解下如何在 MFC 中使用 OpenGL。

编写 OpenGL 程序,简单来说,要做三件事:初始化 OpenGL、绘制图形、当窗口大小改变时重置视口。
下面分别简要介绍一下,详细说明可参阅这篇文章《MFC中使用OpenGL》。

初始化 OpenGL

MFC 使用 DC(Device Context)绘图,OpenGL 使用 RC(Render Context)绘图,为了将 OpenGL 的图形绘制到 MFC 窗口上,需要在 RC 与 DC 之间建立关联。

Windows 提供了一些扩展函数,用于支持 OpenGL,见《OpenGL 的 Windows 扩展参考》。
可通过调用 OpenGL 的 Windows 扩展函数 wglCreateContext,以 DC 为参数,创建 RC。

在调用 wglCreateContext 创建 RC 之前,需要先设置 DC 的像素格式。
Windows 提供了 PIXELFORMATDESCRIPTOR 结构 来描述像素格式。
我们需要先定义一个 PIXELFORMATDESCRIPTOR 对象来描述像素格式,然后调用 SetPixelFormat 函数设置指定 DC 的像素格式。

在使用 OpenGL 绘图之前,需要先设置当前 RC。
调用 Windows 函数 wglMakeCurrent 设置当前 RC。wglMakeCurrent 的参数 DC 与 wglCreateContext 的参数 DC 可以不是同一个 DC,但这两个 DC 必须位于同一设备上并且具有相同像素格式。
本文没有特别的需求,所以 wglCreateContextwglMakeCurrent 使用同一个 DC。

详见《呈现上下文函数》。

OpenGL 的初始化只需要在窗口创建时执行一次即可。对于对话框程序,可以在 OnInitDialog() 函数里执行。

绘制图形

绘制 OpenGL 图形,是在窗口每次重绘时绘制。对于对话框程序,是在 WM_PAINT 消息的处理函数 OnPaint() 里执行绘图代码。

重置视口大小

OpenGL 绘图,是绘制在视口(Viewport)里。默认的视口大小,是 初始窗口的客户区 大小。
但当窗口大小改变时,OpenGL 视口大小并不会随窗口大小自动改变,所以需要在每次窗口大小改变时,重新设置视口大小。
对于对话框程序,需要在 WM_SIZE 消息的处理函数 OnSize() 里调用 OpenGL 函数 glViewport() 重新设置视口大小。

这篇文章《OpenGL之glViewPort函数的用法》有助于对 OpenGL 视口的理解。

创建 MFC 对话框项目

启动 Visual Studio 2022,选择【创建新项目】:

在这里插入图片描述

在这里插入图片描述
选择 C++ -> Windows -> MFC 应用,然后点击 下一步

在这里插入图片描述
输入 项目名称,然后点击 创建 按钮:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

最后点击 完成 按钮,生成项目如下图所示:

在这里插入图片描述

删掉窗口上自动添加的【TODO】标签、【确定】、【取消】按钮,保存之后,关闭对话框编辑界面。

添加 OpenGL 头文件和库文件

打开 framework.h 文件,在末尾添加下面 4 行代码:

#include <gl\gl.h>			// Header File For The OpenGL32 Library
#include <gl\glu.h>			// Header File For The GLu32 Library

#pragma comment(lib, "OpenGL32.lib")
#pragma comment(lib, "GLU32.lib")

初始化 OpenGL

打开 OpenGLCubeDemoDlg.h 文件,添加下面两个函数声明:

    bool InitializeOpenGL(HDC hDC);		//初始化 OpenGL
    bool SetDCPixelFormat(HDC hDC);		//设置 DC 像素格式

再打开 OpenGLCubeDemoDlg.cpp 文件,在末尾添加上面两个函数的定义:

bool COpenGLCubeDemoDlg::InitializeOpenGL(HDC hDC)
{
	//设置 DC 像素格式
	if (false == SetDCPixelFormat(hDC))
	{
		return false;
	}

	//创建 RC
	HGLRC hRC = wglCreateContext(hDC);
	if (hRC == NULL)
	{
		return false;
	}
	//为当前线程设置 RC 
	if (wglMakeCurrent(hDC, hRC) == FALSE)
	{
		return false;
	}

	glClearDepth(1.0f);
	
	glEnable(GL_TEXTURE_2D);								// Enable Texture Mapping
	glEnable(GL_DEPTH_TEST);								// Enables Depth Testing
	glDepthFunc(GL_LEQUAL);									// The Type Of Depth Testing To Do
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);		// Really Nice Perspective Calculations
	return true;
}
bool COpenGLCubeDemoDlg::SetDCPixelFormat(HDC hDC)
{
	static PIXELFORMATDESCRIPTOR pfd =
	{
		sizeof(PIXELFORMATDESCRIPTOR),	//pfd结构的大小
		1,								//版本号
		PFD_DRAW_TO_WINDOW |			//支持在窗口中绘图
		PFD_SUPPORT_OPENGL |			//支持OpenGL
		PFD_DOUBLEBUFFER,				//支持双缓冲
		PFD_TYPE_RGBA,					//RGBA颜色模式
		32,								//32位颜色深度
		0, 0, 0, 0, 0, 0,				//忽略颜色位
		0,								//没有非透明度缓存
		0,								//忽略移位位
		0,								//无累计缓存
		0, 0, 0, 0,						//忽略累计位
		32,								//32位深度缓存
		0,								//无模板缓存
		0,								//无辅助缓存
		PFD_MAIN_PLANE,					//主层
		0,								//保留
		0, 0, 0							//忽略层,可见性和损毁掩模
	};

	//得到 DC 最匹配的像素格式
	int pixelFormat = ChoosePixelFormat(hDC, &pfd);
	if (0 == pixelFormat)
	{
		//如果没有找到,就调用 DescribePixelFormat 函数来选择索引值为 1 的像素格式
		pixelFormat = 1;
		if (DescribePixelFormat(hDC, pixelFormat, sizeof(PIXELFORMATDESCRIPTOR), &pfd) == 0)
		{
			MessageBox(_T("ChoosePixelFormat 失败"));
			return false;
		}
	}
	//设置 DC 像素格式
	if (SetPixelFormat(hDC, pixelFormat, &pfd) == FALSE)
	{
		MessageBox(_T("SetPixelFormat 失败"));
		return false;
	}
	return true;
}

注意上面像素格式的定义,最主要的是第三个参数 dwFlags,这里设置了三个值 PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,每个值的作用,请见代码注释或 PIXELFORMATDESCRIPTOR 文档。

然后找到 COpenGLCubeDemoDlg::OnInitDialog() 函数,添加下面这行代码:

InitializeOpenGL(this->GetDC()->GetSafeHdc());

做完上面的工作后,就完成了 OpenGL 的初始化。

此时运行程序,还是一个空白窗口。
下面进入本文的重点,OpenGL 绘图。

画一个正方形

在绘制立方体前,先画一个正方形练练手,熟悉一下 OpenGL 的绘图步骤。

OpenGLCubeDemoDlg.h 文件里,添加函数声明:

void DrawRect();

OpenGLCubeDemoDlg.cpp 文件里,在末尾添加函数定义:

void COpenGLCubeDemoDlg::DrawRect()
{
	glBegin(GL_QUADS);
	glVertex2f(-0.5, 0.5);
	glVertex2f(-0.5, -0.5);
	glVertex2f(0.5, -0.5);
	glVertex2f(0.5, 0.5);
	glEnd();
}

然后找到 COpenGLCubeDemoDlg::OnPaint() 函数,在 CDialogEx::OnPaint(); 语句后面,添加下面代码:

		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glLoadIdentity();
		DrawRect();
		glFlush();
		SwapBuffers(wglGetCurrentDC());

Ctrl+F5 运行程序,如下图:

在这里插入图片描述

下面重点说一说 DrawRect() 函数。
DrawRect() 里共有 6 行代码,使用了 3 个函数。
其中 glBegin()glEnd() 要成对出现,glBegin() 的参数 GL_QUADS 表示要画一个四边形。
有关 glBegin() 函数及其参数的详细说明,请见 glBegin 函数。
对于 glBegin() 的参数,下面这张图,看起来更直观:

在这里插入图片描述

另一个是 glVertex2f() 函数,用于指定图形的顶点。由于正方形有 4 个顶点,所以调用了 4glVertex2f() 函数,指定了 4 个顶点。
glVertex 有一系列函数,只是参数不同,在函数名里以参数个数和参数类型作为后缀来区分,详细说明请见 glVertex 函数。

有关 OpenGL 函数的命名规则,请见下图:

在这里插入图片描述

glVertex2f() 函数的参数,是顶点的 (x, y) 坐标,这里设置的都是 0.5。要想理解 0.5 的含义,需要先搞清楚 OpenGL 的坐标系。

OpenGL 坐标系

OpenGL 是右手坐标系,X 轴正方向指向屏幕右侧,Y 轴正方向指向屏幕上方,Z 轴正方向指向屏幕外,如下图:

在这里插入图片描述

OpenGL 坐标系的原点 (0, 0, 0) 点位于屏幕中心,屏幕左下角的坐标是 (-1, -1, 0),右上角的坐标是 (1, 1, 0),如下图:

在这里插入图片描述

所以,DrawRect() 函数里指定的 4 个顶点,分别位于距离屏幕各边的 1/4 处。从上面的截图里,我们看到,也确实是这样的位置。

需要说明的是,指定顶点时,各个顶点需要按照顺时针方向或逆时针方向顺序排列。

DrawRect() 函数里,是从左上角开始,按照逆时针方向指定的。

	glBegin(GL_QUADS);
	glVertex2f(-0.5, 0.5);		//左上角 
	glVertex2f(-0.5, -0.5);		//左下角
	glVertex2f(0.5, -0.5);		//右下角
	glVertex2f(0.5, 0.5);		//右上角
	glEnd();

在这里,我们要画的是一个正方形,从指定的顶点位置来看,也应该是正方形,但从上面的截图里看到的,却是长方形。
其实稍微想一下,我们就会想到,如果把窗口变成正方形,那么里面画的图形,就也是正方形了。

在这里插入图片描述

但这并不是我们想要的结果,我们希望不论窗口大小如何,里面画的始终都是正方形。这个问题,留待重置视口大小时一并解决。

改变默认颜色

从上面的截图中可以看到,OpenGL 默认的背景色是 黑色,前景色是 白色

修改背景色,可在调用 glClear() 函数前,先调用 glClearColor() 函数。
如在 COpenGLCubeDemoDlg::OnPaint() 函数中,添加 glClearColor(0.0f, 0.05f, 0.15f, 1.0f); 语句,可将背景色改为夜空蓝色。

修改前景色,可在指定点坐标之前,先调用 glColor 函数。glColor 也是一系列函数,详见 glColor 函数。
如在 COpenGLCubeDemoDlg::DrawRect() 函数里,在 glBegin(GL_QUADS); 之前,添加 glColor3ub(96, 0, 0); 语句,可将四边形改为深红色。

在这里插入图片描述

重置视口大小

此时如果改变窗口大小,会发现四边形并不在窗口中央,这是由于改变窗口大小时,没有同时改变 OpenGL 视口导致的。

点击菜单 视图 -> 类视图,打开 类视图 窗口。在 COpenGLCubeDemoDlg 节点上,点击鼠标右键,弹出快捷菜单:

在这里插入图片描述

在快捷菜单上,选择 属性 项,打开 属性 窗口。
属性 窗口的工具栏上,点击 消息 按钮,然后找到 WM_SIZE 消息,点击右侧下拉箭头,在下拉框里选择 <add>OnSize,如下图:

在这里插入图片描述

此时会自动在 COpenGLCubeDemoDlg.cpp 文件里添加 OnSize 函数定义。

然后在 OnSize() 函数里,添加 glViewport(0, 0, cx, cy); 语句,再重新运行程序,可看到改变窗口大小后,四边形仍然位于窗口中央。

void COpenGLCubeDemoDlg::OnSize(UINT nType, int cx, int cy)
{
	CDialogEx::OnSize(nType, cx, cy);
	
	glViewport(0, 0, cx, cy);
}

但是多操作几次会发现,并不是每次改变窗口大小时,四边形都能位于窗口中央。感觉这是窗口刷新不及时导致的,在 glViewport(0, 0, cx, cy); 之后添加一行 Invalidate(); 强制刷新窗口,然后重新运行程序,再次调整窗口大小,发现这回正常了。

还有一个问题,就是我们想画的是正方形,但目前为止看到的都是长方形。
有两个办法可以解决这个问题,使用任何一个都可以:

  • 根据窗口宽高比例调整视口位置,并设置视口宽高相同。
void COpenGLCubeDemoDlg::OnSize(UINT nType, int cx, int cy)
{
	CDialogEx::OnSize(nType, cx, cy);

	GLint nX = 0, nY = 0, nWidth = 0;
	if (cx < cy)
	{
		nY = (cy - cx) / 2;
		nWidth = cx;
	}
	else
	{
		nX = (cx - cy) / 2;
		nWidth = cy;
	}
	glViewport(nX, nY, nWidth, nWidth);

	Invalidate();
}
  • 仍保持视口与窗口大小相同,使用 glOrtho 或 gluOrtho2D 函数设置视景体。
void COpenGLCubeDemoDlg::OnSize(UINT nType, int cx, int cy)
{
	CDialogEx::OnSize(nType, cx, cy);

	glViewport(0, 0, cx, cy);
	
	if (cy == 0)									//防止除0
		cy = 1;
	GLfloat scale = (GLfloat)cx / (GLfloat)cy;		//窗口协调比例

	glMatrixMode(GL_PROJECTION);				//重置投影矩阵,告诉OpenGL接下来做投影变换
	glLoadIdentity();

	if (cx < cy)
	{
		gluOrtho2D(-1.0, 1.0, -1.0 / scale, 1.0 / scale);
	}
	else
	{
		gluOrtho2D(-1.0 * scale, 1.0 * scale, -1.0, 1.0);
	}

	//告诉openGL未来的转换将影响绘制的图形
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	Invalidate();
}

这里必须说明一下,在上面的代码中可以看到,在调用 gluOrtho2D 之前和之后,都调用了 glMatrixModeglLoadIdentity 函数。
glMatrixMode 函数 指明接下来的代码操作的是哪个矩阵,详细说明可见这篇文章《OpenGL之glMatrixMode函数的用法》。
glLoadIdentity 函数 将当前矩阵复位到初始状态。

大家可能看到了,程序启动后,窗口默认是最大化的,此时显示的四边形仍然还是长方形。如果你看到了这个现象,那不要紧,只要把 COpenGLCubeDemoDlg::OnInitDialog() 函数里的 ShowWindow(SW_MAXIMIZE); 一行挪到 InitializeOpenGL(this->GetDC()->GetSafeHdc()); 后面即可。

下面看下最终效果,然后进入本文的正题:绘制立方体。

在这里插入图片描述

绘制立方体

通过上面画正方形,我们对 OpenGL 的绘图步骤有了初步了解,现在开始绘制一个立方体。有了画正方形的知识,再绘制立方体就容易多了。
首先来说,立方体是由六个面构成的,每个面都是一个正方形。其次,相对于正方形,立方体需要指定顶点的 Z 坐标。

OpenGLCubeDemoDlg.h 文件里,添加函数声明:

void DrawCube();

OpenGLCubeDemoDlg.cpp 文件里,在末尾添加函数定义:

void COpenGLCubeDemoDlg::DrawCube()
{
	glBegin(GL_QUADS);

	// Front Face
	glColor3ub(128, 0, 0);		//红
	glVertex3f(-0.5f, -0.5f, 0.5f);
	glVertex3f(0.5f, -0.5f, 0.5f);
	glVertex3f(0.5f, 0.5f, 0.5f);
	glVertex3f(-0.5f, 0.5f, 0.5f);

	// Back Face
	glColor3ub(128, 128, 0);	//黄
	glVertex3f(-0.5f, -0.5f, -0.5f);
	glVertex3f(0.5f, -0.5f, -0.5f);
	glVertex3f(0.5f, 0.5f, -0.5f);
	glVertex3f(-0.5f, 0.5f, -0.5f);

	// Top Face
	glColor3ub(0, 0, 128);		//蓝
	glVertex3f(-0.5f, 0.5f, 0.5f);
	glVertex3f(0.5f, 0.5f, 0.5f);
	glVertex3f(0.5f, 0.5f, -0.5f);
	glVertex3f(-0.5f, 0.5f, -0.5f);

	// Bottom Face
	glColor3ub(128, 0, 128);	//紫
	glVertex3f(-0.5f, -0.5f, 0.5f);
	glVertex3f(0.5f, -0.5f, 0.5f);
	glVertex3f(0.5f, -0.5f, -0.5f);
	glVertex3f(-0.5f, -0.5f, -0.5f);

	// Left Face
	glColor3ub(0, 128, 0);		//绿
	glVertex3f(-0.5f, -0.5f, -0.5f);
	glVertex3f(-0.5f, -0.5f, 0.5f);
	glVertex3f(-0.5f, 0.5f, 0.5f);
	glVertex3f(-0.5f, 0.5f, -0.5f);

	// Right face
	glColor3ub(0, 128, 128);	//青
	glVertex3f(0.5f, -0.5f, 0.5f);
	glVertex3f(0.5f, -0.5f, -0.5f);
	glVertex3f(0.5f, 0.5f, -0.5f);
	glVertex3f(0.5f, 0.5f, 0.5f);

	glEnd();
}

DrawCube() 函数的代码与 DrawRect() 对比一下,可以看到 DrawCube() 里共指定了 24 个顶点,每个面 4 个顶点。而且指定顶点时不再使用 glVertex2f() 函数,而是使用 glVertex3f() 函数,增加了 Z 坐标。

由于之前绘制的是正方形,接下来还需要修改一下 OnPaint()OnSize() 函数,改为绘制立方体。
首先把 OnPaint() 函数中的 DrawRect(); 一行替换为 DrawCube();,然后再删改 OnSize() 函数代码如下:

void COpenGLCubeDemoDlg::OnSize(UINT nType, int cx, int cy)
{
	CDialogEx::OnSize(nType, cx, cy);

	glViewport(0, 0, cx, cy);
	Invalidate();
}

重新编译并运行程序,显示窗口如下图:

在这里插入图片描述

在这个窗口里,我们没有看到立方体,只看到了一个黄色的长方形。要理解这是怎么回事,需要先了解下 视点

  • 视点:即 观察点,也可以理解为 人眼摄像机 的位置,默认坐标是 (0,0,0),即坐标系的原点,也是我们绘制的这个立方体的中心点。对于 人眼摄像机,还要有 视线方向,默认的视线方向朝向 Z 轴的负方向,即屏幕里面。

了解了 视点 之后,还需要了解一点:

OpenGL 绘制的物体是空心的。

现在我们想象一下,我们的眼睛处在视点的位置,也就是立方体的内部,位于正中心点,向 Z 轴负方向(指向屏幕内部)看去,立方体的前面在我们脑后,立方体的背面在视线前方,那么我们看到的黄色长方形正是立方体的背面。

那如何我们才能看到立方体呢?还需要再了解下 视景体

  • 视景体:也称 视锥体,可以理解为 人眼摄像机 的视野范围。视野外的物体我们是看不到的,同样的,视景体外的物体,OpenGL 也不会绘制出来。OpenGL 默认的视景体(x: [-1, 1], y: [-1, 1], z: [-1, 1]) 的范围。

为了看到完整的立方体,我们可以使用 gluLookAt 函数把 视点 移到立方体的外面。不过这里我们使用另一个办法,通过 投影变换视景体移到眼前,然后再把立方体移动到视景体里。

投影有两种:正交投影透视投影。正交投影的变换函数是 glOrtho,前面已经使用过了。正交投影没有透视效果,物体在远处和近处,大小是一样的。为了使立方体看起来更有立体感,我们将使用透视投影。

透视投影 的变换函数有 glFrustum 和 gluPerspective。这篇文章《OpenGL 入门纪录–2 .透视函数glFrustum(), gluPerspective()函数用法和glOrtho()函数用法》对这两个函数有更详细的介绍。

下面两幅图,对这两个函数的参数做了直观的说明,仔细理解这两幅图,对于我们正确理解这两个函数参数的含义非常有帮助。

glFrustum()参数含义

gluPerspective()参数含义

上面的图描述了 glFrustum 函数 参数的含义,下面的图描述了 gluPerspective 函数 参数的含义。

注意:这两个函数的最后两个参数 zNear 和 zFar 分别表示相机到近裁面和远裁面的距离,始终为正值,必须大于 0。

这两个函数可以相互转换,可以参考这篇文章《OpenGL中gluPerspective函数和glFrustum函数的关系》。

我们接下来将使用相对简单的 gluPerspective 函数 进行投影变换(改变视景体的大小和位置)。
修改 OnSize() 函数如下:

void COpenGLCubeDemoDlg::OnSize(UINT nType, int cx, int cy)
{
	CDialogEx::OnSize(nType, cx, cy);

	glViewport(0, 0, cx, cy);

	glMatrixMode(GL_PROJECTION);		//重置投影矩阵,告诉OpenGL接下来做投影变换
	glLoadIdentity();

	gluPerspective(45.0f, (GLfloat)cx / (GLfloat)cy, 1.0f, 100.0f);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	Invalidate();
}

这里再强调一下,在做投影变换前和变换后都要调用 glMatrixModeglLoadIdentity 函数 。

现在再重新编译、运行程序,会发现不但没有立方体,而且原来的黄色长方形也不见了。
这是由于经过投影变换后,视景体已经移到了 Z 轴的 [-1, -100] 之间,而我们绘制的立方体还位于 Z 轴的 [0.5, -0.5] 之间。因为立方体不在视景体内,所以 OpenGL 根本就不会绘制这个立方体。

接下来,我们在 DrawCube() 函数的 glBegin(GL_QUADS); 语句前添加下面一行:

glTranslatef(0.0f, 0.0f, -2.5);

glTranslatef 是平移变换函数,这行语句的作用是将立方体向 Z 轴的负方向平移 2.5 个单位距离。由于立方体原来位于 Z 轴的 [0.5, -0.5] 之间,移动 -2.5 距离后,立方体的位置就变为了 Z 轴的 [-2, -3] 之间,这样就把立方体移到了视景体里面。

再重新编译、运行程序,会看到一个红色的正方形,对照代码可以发现,这个红色的正方形是立方体的正面:

在这里插入图片描述

通过透视投影和平移变换之后,虽然我们还没有看到完整的立方体,但其实已经前进了一大步。现在立方体已经在视景体里了,只是由于视点的视线方向正对立方体中心,而且透视投影使得立方体的背面更小,所以立方体的其余部分,是被正面遮挡了,因此我们才看不到。现在只要旋转一下立方体,我们就可以看到其余部分了。

使用箭头按键旋转立方体

OpenGLCubeDemoDlg.h 文件里,添加如下变量声明,分别表示绕 X 轴和绕 Y 轴的旋转角度:

	float m_rotationX = 0.0f;
	float m_rotationY = 0.0f;

然后再修改 DrawCube() 函数,在 glBegin(GL_QUADS); 语句前添加如下两行,使立方体绕 X 轴和 Y 轴旋转指定角度:

	glRotatef(m_rotationX, 1.0f, 0.0f, 0.0f);
	glRotatef(m_rotationY, 0.0f, 1.0f, 0.0f);

接下来增加按键处理,上下箭头改变绕 X 轴旋转的角度,左右箭头改变绕 Y 轴旋转的角度,Esc键取消旋转。

点击菜单 视图 -> 类视图,打开 类视图 窗口。在 COpenGLCubeDemoDlg 节点上,点击鼠标右键,弹出快捷菜单。在快捷菜单上,选择 属性 项,打开 属性 窗口。
属性 窗口的工具栏上,点击 重写 按钮,然后找到 PreTranslateMessage 项,点击右侧下拉箭头,在下拉框里选择 <add>PreTranslateMessage,如下图:

在这里插入图片描述

此时会自动在 COpenGLCubeDemoDlg.cpp 文件里添加 PreTranslateMessage 函数定义。

修改 PreTranslateMessage 函数如下:

BOOL COpenGLCubeDemoDlg::PreTranslateMessage(MSG* pMsg)
{
	if (pMsg->message == WM_KEYDOWN)
	{
		if (pMsg->wParam == VK_LEFT)
		{
			m_rotationY -= 1;
		}
		else if (pMsg->wParam == VK_RIGHT)
		{
			m_rotationY += 1;
		}
		else if (pMsg->wParam == VK_UP)
		{
			m_rotationX -= 1;
		}
		else if (pMsg->wParam == VK_DOWN)
		{
			m_rotationX += 1;
		}
		else if (pMsg->wParam == VK_ESCAPE)
		{
			m_rotationX = 0.0f;
			m_rotationY = 0.0f;
			return FALSE;
		}
	}
	return CDialogEx::PreTranslateMessage(pMsg);
}

然后重新编译、运行程序,按键盘上的上、下、左、右箭头旋转立方体,可是发现立方体并没有旋转,看到的仍然是一个红色的正方形。

这个问题有两个解决办法:

  1. 去掉 OnPaint() 函数里的 CDialogEx::OnPaint(); 一行;
  2. PreTranslateMessage(MSG* pMsg) 函数里,每次改变旋转角度后调用 Invalidate();

第一种方法,去掉 CDialogEx::OnPaint(); 后,窗口会不停刷新,所以改变旋转角度后,立即就绘制出了旋转后的立方体。
第二种方法,添加 Invalidate(); ,可在改变旋转角度后,强制窗口刷新。

为简单起见,使用第一种方法,去掉 OnPaint() 函数里的 CDialogEx::OnPaint(); 。(其实这么做是不对的,后面会讲到

再次重新编译、运行程序,按键盘上的上、下、左、右箭头旋转立方体,这回终于看到真正的立方体了。

在这里插入图片描述

深度测试

InitializeOpenGL() 函数里,有这样一行代码,我们没有仔细讲解:

glEnable(GL_DEPTH_TEST);

现在大家可以找到这行代码,把它注释掉,然后重新编译并运行程序,按键盘的上、下、左、右箭头旋转立方体,看看与先前有什么不同。
想仔细了解的同学,可以打开这个教程 坐标系统,在页面中搜索 Z缓冲 字样,然后仔细阅读相关内容。

添加纹理

上面我们绘制了一个彩色的立方体,我们还可以给立方体贴上图片,使细节更丰富。

可以阅读这个教程,了解 纹理。

这个教程推荐使用 stb_image.h 加载图片。stb_image.h 是一个非常流行的单头文件图像加载库,它能够加载大部分流行的文件格式。stb_image.h 可以在这里下载。

下载 stb_image.h 文件,并将它复制到 D:\OpenGLCubeDemo\OpenGLCubeDemo 文件夹内。

解决方案资源管理器 中,选中工具栏中的 显示所有文件 按钮。再在下面的文件列表中,在 stb_image.h 节点上点击鼠标右键,在弹出菜单中选择 包括在项目中(J)。然后取消工具栏中 显示所有文件 按钮的选中状态。

在这里插入图片描述

OpenGLCubeDemoDlg.cpp 文件顶部添加:

#define STBI_WINDOWS_UTF8
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

OpenGLCubeDemoDlg.h 文件里,添加变量和函数声明:

UINT m_glTexture = 0;
UINT LoadGLTexture();

COpenGL11DemoDlg::OnInitDialog() 函数里调用 LoadGLTexture() 加载纹理:

m_glTexture = LoadGLTexture();

函数 LoadGLTexture() 定义如下:

UINT COpenGLCubeDemoDlg::LoadGLTexture()
{
	stbi_set_flip_vertically_on_load(true);
	
	int width, height, nrChannels;
	unsigned char* data = stbi_load(IMAGE_PATH, &width, &height, &nrChannels, 0);

	unsigned int texture;
	glGenTextures(1, &texture);
	glBindTexture(GL_TEXTURE_2D, texture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);   //在纹理被放大时使用线性过滤
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);  //在纹理被缩小时使用邻近过滤
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);

	stbi_image_free(data);
	return texture;
}

glTexImage2D() 函数用于将内存中的数据拷贝到 OpenGL 纹理单元中,参数很多,我们尤其需要注意第 3 个和第 7 个参数:

  • 第 3 个参数,告诉 OpenGL 我们希望把纹理储存为何种格式
  • 第 7 个参数,是指源图的格式
  • 当纹理为 24 位图片时,这两个参数应设置为 GL_RGB
  • 当纹理为 32 位图片时,这两个参数应设置为 GL_RGBA

对于纹理图片,有以下几点经验可供参考:

  • 尽量使用 JPG 或 PNG 格式的图片
  • 尽量使用 72 dpi 或 96 dpi 的 24 位或 32 位的图片
  • 图片的宽度和高度应为 2 的整数倍
  • 对于 24 位图片,高度应为宽度的整数倍,或者宽度为高度的整数倍

应用纹理

我们接下来使用下面这张图片,将其贴到立方体的每个面上。

在这里插入图片描述

这需要修改 DrawCube() 函数,调用 glBindTexture() 函数 绑定纹理,并在每个顶点处使用 glTexCoord 函数 指定纹理坐标。

void COpenGLCubeDemoDlg::DrawCube()
{
	glTranslatef(0.0f, 0.0f, -2.5);

	glRotatef(m_rotationX, 1.0f, 0.0f, 0.0f);
	glRotatef(m_rotationY, 0.0f, 1.0f, 0.0f);

	glBindTexture(GL_TEXTURE_2D, m_glTexture);

	glBegin(GL_QUADS);

	// Front Face
	glTexCoord2i(0, 0);
	glVertex3f(-0.5f, -0.5f, 0.5f);
	glTexCoord2i(1, 0);
	glVertex3f(0.5f, -0.5f, 0.5f);
	glTexCoord2i(1, 1);
	glVertex3f(0.5f, 0.5f, 0.5f);
	glTexCoord2i(0, 1);
	glVertex3f(-0.5f, 0.5f, 0.5f);

	// Back Face
	glTexCoord2i(1, 0);
	glVertex3f(-0.5f, -0.5f, -0.5f);
	glTexCoord2i(0, 0);
	glVertex3f(0.5f, -0.5f, -0.5f);
	glTexCoord2i(0, 1);
	glVertex3f(0.5f, 0.5f, -0.5f);
	glTexCoord2i(1, 1);
	glVertex3f(-0.5f, 0.5f, -0.5f);

	// Top Face
	glTexCoord2i(0, 0);
	glVertex3f(-0.5f, 0.5f, 0.5f);
	glTexCoord2i(1, 0);
	glVertex3f(0.5f, 0.5f, 0.5f);
	glTexCoord2i(1, 1);
	glVertex3f(0.5f, 0.5f, -0.5f);
	glTexCoord2i(0, 1);
	glVertex3f(-0.5f, 0.5f, -0.5f);

	// Bottom Face
	glTexCoord2i(0, 0);
	glVertex3f(-0.5f, -0.5f, 0.5f);
	glTexCoord2i(1, 0);
	glVertex3f(0.5f, -0.5f, 0.5f);
	glTexCoord2i(1, 1);
	glVertex3f(0.5f, -0.5f, -0.5f);
	glTexCoord2i(0, 1);
	glVertex3f(-0.5f, -0.5f, -0.5f);

	// Left Face
	glTexCoord2i(0, 0);
	glVertex3f(-0.5f, -0.5f, -0.5f);
	glTexCoord2i(1, 0);
	glVertex3f(-0.5f, -0.5f, 0.5f);
	glTexCoord2i(1, 1);
	glVertex3f(-0.5f, 0.5f, 0.5f);
	glTexCoord2i(0, 1);
	glVertex3f(-0.5f, 0.5f, -0.5f);

	// Right face
	glTexCoord2i(0, 0);
	glVertex3f(0.5f, -0.5f, 0.5f);
	glTexCoord2i(1, 0);
	glVertex3f(0.5f, -0.5f, -0.5f);
	glTexCoord2i(1, 1);
	glVertex3f(0.5f, 0.5f, -0.5f);
	glTexCoord2i(0, 1);
	glVertex3f(0.5f, 0.5f, 0.5f);

	glEnd();
}

这里需要着重说一下纹理坐标。纹理坐标的 (0, 0) 点位于左下角,(1, 1) 点位于右上角,如下图所示:

在这里插入图片描述

重新编译、运行程序,按键盘的上、下、左、右箭头旋转立方体,效果如下图:

在这里插入图片描述

换一个纹理

我们换下面这张图片作为纹理,使立方体看起来像一个骰子。

在这里插入图片描述

需要修改 DrawCube() 函数内的每个顶点的纹理坐标。注意,由于要使用小数,纹理坐标函数换成了 glTexCoord2f()

void COpenGLCubeDemoDlg::DrawCube()
{
	glTranslatef(0.0f, 0.0f, -2.5);

	glRotatef(m_rotationX, 1.0f, 0.0f, 0.0f);
	glRotatef(m_rotationY, 0.0f, 1.0f, 0.0f);

	glBindTexture(GL_TEXTURE_2D, m_glTexture);

	glBegin(GL_QUADS);

	// Front Face
	glTexCoord2f(0.0f, 0.5f);
	glVertex3f(-0.5f, -0.5f, 0.5f);
	glTexCoord2f(0.33f, 0.5f);
	glVertex3f(0.5f, -0.5f, 0.5f);
	glTexCoord2f(0.33f, 1.0f);
	glVertex3f(0.5f, 0.5f, 0.5f);
	glTexCoord2f(0.0f, 1.0f);
	glVertex3f(-0.5f, 0.5f, 0.5f);

	// Back Face
	glTexCoord2f(0.66f, 0.0f);
	glVertex3f(-0.5f, -0.5f, -0.5f);
	glTexCoord2f(1.0f, 0.0f);
	glVertex3f(0.5f, -0.5f, -0.5f);
	glTexCoord2f(1.0f, 0.5f);
	glVertex3f(0.5f, 0.5f, -0.5f);
	glTexCoord2f(0.66f, 0.5f);
	glVertex3f(-0.5f, 0.5f, -0.5f);

	// Top Face
	glTexCoord2f(0.33f, 0.5f);
	glVertex3f(-0.5f, 0.5f, 0.5f);
	glTexCoord2f(0.66f, 0.5f);
	glVertex3f(0.5f, 0.5f, 0.5f);
	glTexCoord2f(0.66f, 1.0f);
	glVertex3f(0.5f, 0.5f, -0.5f);
	glTexCoord2f(0.33f, 1.0f);
	glVertex3f(-0.5f, 0.5f, -0.5f);

	// Bottom Face
	glTexCoord2f(0.33f, 0.0f);
	glVertex3f(-0.5f, -0.5f, 0.5f);
	glTexCoord2f(0.66f, 0.0f);
	glVertex3f(0.5f, -0.5f, 0.5f);
	glTexCoord2f(0.66f, 0.5f);
	glVertex3f(0.5f, -0.5f, -0.5f);
	glTexCoord2f(0.33f, 0.5f);
	glVertex3f(-0.5f, -0.5f, -0.5f);

	// Left Face
	glTexCoord2f(0.66f, 0.5f);
	glVertex3f(-0.5f, -0.5f, -0.5f);
	glTexCoord2f(1.0f, 0.5f);
	glVertex3f(-0.5f, -0.5f, 0.5f);
	glTexCoord2f(1.0f, 1.0f);
	glVertex3f(-0.5f, 0.5f, 0.5f);
	glTexCoord2f(0.66f, 1.0f);
	glVertex3f(-0.5f, 0.5f, -0.5f);

	// Right face
	glTexCoord2f(0.0f, 0.0f);
	glVertex3f(0.5f, -0.5f, 0.5f);
	glTexCoord2f(0.33f, 0.0f);
	glVertex3f(0.5f, -0.5f, -0.5f);
	glTexCoord2f(0.33f, 0.5f);
	glVertex3f(0.5f, 0.5f, -0.5f);
	glTexCoord2f(0.0f, 0.5f);
	glVertex3f(0.5f, 0.5f, 0.5f);

	glEnd();
}

再次重新编译、运行程序,按键盘的上、下、左、右箭头旋转立方体,效果如下图:

在这里插入图片描述

自动旋转

在上面代码中,我们通过按键盘的上、下、左、右箭头改变 m_rotationXm_rotationY 变量的值,来旋转立方体。
下面我们使用定时器,定时改变 m_rotationXm_rotationY 变量的值,来实现立方体自动旋转。

解决方案资源管理器 中,在项目名称 OpenGLCubeDemo 上点击鼠标右键,在弹出菜单上选择 类向导(Z)…,打开 类向导 对话框。

在这里插入图片描述

类名 下拉框中选择 COpenGLCubeDemoDlg,切换到 消息 选项卡,在下面消息列表中,选中 WM_TIMER 消息,然后点击右侧的 添加处理程序(A) 按钮。
此时会在 现有处理程序 列表里添加消息处理函数 OnTimer,点击 确定 按钮,会自动在 COpenGLCubeDemoDlg.cpp 文件里添加 OnTimer 函数定义。

修改 OnTimer 函数,定时器每触发一次,立方体分别绕 X 轴和 Y 轴各旋转 1 度:

void COpenGLCubeDemoDlg::OnTimer(UINT_PTR nIDEvent)
{
	m_rotationX += 1;
	m_rotationY += 1;
	Invalidate();

	CDialogEx::OnTimer(nIDEvent);
}

OpenGLCubeDemoDlg.h 文件里,添加如下变量声明,表示定时器是否启动:

bool  m_bTimer = false;

修改 PreTranslateMessage 函数,添加空格键处理,通过按空格键启动/停止定时器:

else if (pMsg->wParam == VK_SPACE)
{
	if (false == m_bTimer)
	{
		SetTimer(1, 100, NULL);
		m_bTimer = true;
	}
	else
	{
		KillTimer(1);
		m_bTimer = false;
	}
}

重新编译、运行程序,发现按空格键后,定时器并没有启动。这是什么原因呢?

还记得最初按上、下、左、右箭头时,立方体没有旋转吧?我们当时选择了去掉 OnPaint() 函数里的 CDialogEx::OnPaint(); 代码来解决这个问题,就是这个操作,导致了现在定时器没有启动,所以我们不能用这个方法了,而应该改用第二种方法,在每次改变旋转角度后调用 Invalidate(); 强制刷新窗口。

至于这个问题产生的原因,就有些复杂了,涉及到了 MFC 和 Win32 的内部逻辑,不感兴趣的小伙伴可以直接跳过。

先从 MFC 说起,CDialogExCDialog 的派生类,实际 CDialogEx::OnPaint() 是调用的 CDialog::OnPaint()
我们到 MFC 源码的 dlgcore.cpp 文件里看下 CDialog::OnPaint() 函数的定义,在 OnPaint() 函数的第一行是一个 CPaintDC 变量定义:CPaintDC dc(this);
再到 wingdi.cpp 里看下 CPaintDC 的构造函数和析构函数,在 CPaintDC 的构造函数里调用了 BeginPaint(),在析构函数里调用了 EndPaint()
关键就在这里,也就是说,我们去掉 OnPaint() 函数里的 CDialogEx::OnPaint(); 一行,就会导致不执行 BeginPaint(),不执行 BeginPaint() 会产生什么后果呢?

来看看微软对 WM_PAINT 消息的说明:

BeginPaint 将窗口的更新区域设置为 NULL。 这会清除该区域,阻止其生成后续 WM_PAINT 消息。 如果应用程序处理 WM_PAINT 消息,但不调用 BeginPaint 或以其他方式清除更新区域,则只要该区域不为空,应用程序将继续接收 WM_PAINT 消息。 在所有情况下,应用程序必须在从 WM_PAINT 消息返回之前清除更新区域。

就是说,不执行 BeginPaint,就会不停接收 WM_PAINT 消息,窗口就会不停刷新。

另外,WM_TIMER 是低优先级消息,当窗口不停处理 WM_PAINT 消息时,WM_TIMER 就得不到及时处理,所以定时器就没有触发。

现在我们应该明白了,去掉 OnPaint() 函数里的 CDialogEx::OnPaint(); 是错误的,不应该这样做。

MFC 源码默认位于类似这样的目录:C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\atlmfc\src\mfc

销毁资源

参照添加 WM_TIMER 消息处理函数的方法,添加 WM_DESTROY 消息的处理函数。

void COpenGLCubeDemoDlg::OnDestroy()
{
	CDialogEx::OnDestroy();

	wglMakeCurrent(NULL, NULL);			// Make the rendering context not current 

	if (m_glTexture != 0)
	{
		glDeleteTextures(1, &m_glTexture);		// If valid gltexture delete it
	}
}

更进一步

这篇文章只是一篇 OpenGL 入门教程,用于了解 OpenGL 的一些基本概念,如果大家想深入学习 OpenGL,接下来可以学习这个教程 LearnOpenGL CN。

参考

  • 原生 Win32 API 实现 OpenGL 教程(第一部分)
  • Native Win32 API OpenGL Tutorial - Part 1
  • Native Win32 API OpenGL Tutorial - Part 2
  • MFC中使用OpenGL
  • OpenGL在MFC中的使用总结(一)——基本框架
  • OpenGL之glViewPort函数的用法
  • OpenGL之glMatrixMode函数的用法
  • OpenGL 入门纪录–2 .透视函数glFrustum(), gluPerspective()函数用法和glOrtho()函数用法
  • OpenGL中gluPerspective函数和glFrustum函数的关系
  • OpenGL中glFrustum()和gluPerspective()的相互转换
  • OpenGL(3) ->窗口,视口,裁剪区,视景体

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/316352.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

0104 AJAX介绍

Ajax 的全称是 Asynchronous Javascript And XML &#xff08;异步 JavaScript 和 XML &#xff09;。 通俗的理解&#xff1a;在网页中利用 XMLHttpRequest 对象和服务器进行数据交互的方式&#xff0c;就是 Ajax Ajax 能让我们轻松实现网页与服务器之间的数据交互。 浏览器…

基础篇_数据持久化(实战-我的B站,MySQL数据库)

文章目录 一. 实战-我的B站1. 功能演示2. 设计数据类数据展示路径参数 3. 设计 Service 类静态资源映射读取文件的时机Stream API 改进 二. MySQL 数据库1. 数据库必要性2. MySQL 安装下载压缩包初始化数据库运行服务器运行客户端 3. 初步使用4. datagrip添加数据源导入数据用 …

【网络安全】【密码学】【北京航空航天大学】实验四、古典密码(上)【C语言实现】

实验四、古典密码&#xff08;上&#xff09; 一、实验目的 1、 通过本次实验&#xff0c;了解古典加密算法的主要思想&#xff0c;掌握常见的古典密码。 2、 学会应用古典密码&#xff0c;掌握针对部分古典密码的破译方法。 二、原理简介 古典密码的编码方法主要有两种&am…

【深度学习环境搭建】Windows搭建Anaconda3、已经Pytorch的GPU版本

目录 搭建Anaconda3搭建GPU版本的Pytorch你的pip也要换源&#xff0c;推荐阿里源打开conda的PowerShell验证 搭建Anaconda3 无脑下载安装包安装&#xff08;自行百度&#xff09; 注意点&#xff1a; 1、用户目录下的.condarc需要配置&#xff08;自定义环境的地址&#xff08…

【TC3xx芯片】TC3xx芯片电源管理系统PMS详解

目录 前言 正文 1.供电模式选择&#xff08;Supply Mode Selection&#xff09; 1.1 供电域 1.2 供电模式 1.3 供电阈值 1.4 供电上升和下降行为Supply Ramp-up and Ramp-down Behavior 1.5 EVRC产生供电 2. 电源监控 2.1 电源监控原理 2.2 Primary低电压监控 2.3 …

怎么把身份证压缩到200k以下?一分钟教你如图片压缩

在网络平台办理一些业务的时候&#xff0c;经常会需要上传我们的身份证照片&#xff0c;但是大多数平台为了用户体验&#xff0c;会限制上传的图片大小&#xff0c;比如图片不得超过200kb&#xff0c;当我们提交的身份证图片超出限制&#xff0c;就无法顺利提交&#xff1b;这时…

【Docker篇】使用Docker操作镜像

文章目录 &#x1f6f8;镜像&#x1f33a;基本操作⭐docker --help⭐docker pull [ 参数 ]⭐docker images⭐docker save -- 导出⭐docker rmi -- 删除⭐docker load -- 导入 &#x1f6f8;镜像 镜像是指在计算机领域中&#xff0c;通过复制和创建一个与原始对象相似的副本的过…

微信小程序快速入门02(含案例)

&#x1f3e1;浩泽学编程&#xff1a;个人主页 &#x1f525; 推荐专栏&#xff1a;《深入浅出SpringBoot》《java项目分享》 《RabbitMQ》《Spring》《SpringMVC》 &#x1f6f8;学无止境&#xff0c;不骄不躁&#xff0c;知行合一 文章目录 前言一、页面导航1.…

精确掌控并发:分布式环境下并发流量控制的设计与实现(一)

这是《百图解码支付系统设计与实现》专栏系列文章中的第&#xff08;10&#xff09;篇。 本篇主要讲清楚常用的并发流量控制方案&#xff0c;包括固定窗口、滑动窗口、漏桶、令牌桶、分布式消息中间件等&#xff0c;以及各种方案在支付系统不同场景下的应用。 在非支付场景&a…

若依实现前段后登录密码加密

若依虽然有加密解密功能&#xff0c;然后只有前端有&#xff0c;在用户点击保存密码的时候&#xff0c;会将密码保存到本地&#xff0c;但是为了防止密码泄露&#xff0c;所以在保存的时候&#xff0c;进行加密&#xff0c;在回显密码的时候进行解密显示&#xff0c;用户在登录…

上门回收小程序开发,让回收更加简单

资源回收一直是当下深受大众关注的话题&#xff0c;如何做到资源不浪费&#xff0c;成为了大众要考虑的问题。在人们环保意识的加深下&#xff0c;回收行业也是获得了大众的关注&#xff0c;逐渐形成了一个新的商业模式。 随着互联网技术的发展&#xff0c;回收行业也更加方便…

乱码问题汇总

写在前面 在工作中经常会碰到各种莫名其妙的乱码问题&#xff0c;但通过之前的学习&#xff1a;字符集&字符编码-CSDN博客 &#xff0c;可以知道乱码的根本原因就是使用和数据源编码不一样的编码解码导致。 如&#xff1a;BIG5解码GB2312编码内容&#xff0c;编解码不一致…

C++学习笔记(三十五):c++ 函数指针及lambda表达式

本节介绍c函数指针。在一些源码中经常能看到c函数指针&#xff0c;但之前一直觉着这一块比较复杂&#xff0c;就一直没去仔细研究&#xff0c;终于有时间去仔细研究这一块内容了。 c风格的函数指针 函数指针是指将一个函数赋值给一个变量的方法&#xff0c;可以将函数作为一个参…

Oracle篇—实例中和name相关参数的区别和作用

☘️博主介绍☘️&#xff1a; ✨又是一天没白过&#xff0c;我是奈斯&#xff0c;DBA一名✨ ✌✌️擅长Oracle、MySQL、SQLserver、Linux&#xff0c;也在积极的扩展IT方向的其他知识面✌✌️ ❣️❣️❣️大佬们都喜欢静静的看文章&#xff0c;并且也会默默的点赞收藏加关注❣…

可以在微信群里使用midjourney,gpt4,gemini,文心一言4.0,且免费

免费使用gpt4和midjourney 免费使用 参考链接&#xff1a; https://chat.xutongbao.top/

(核心变量)全国上市公司对外开放程度+dofile+参考文献(2000-2022年)

上市公司的对外开放程度数据反映了这些公司在国际市场上的活跃度和全球化程度。这包括了它们的国际贸易参与度、跨国投资和合作、国际市场的营销和品牌推广策略&#xff0c;以及在不同国家和地区的业务布局。此外&#xff0c;这段时间内不同行业和公司的对外开放程度可能有明显…

IDEA新建SpringBoot工程时java版本只有17和21

解决方法&#xff1a;替换源 参考博客&#xff1a;https://www.kuazhi.com/post/712799571.html

VirtualBox安装linuxmint-21.2虚拟机并配置网络

VirtualBox安装linuxmint-21.2虚拟机并配置网络 适用于在VirtualBox平台上安装linuxmint-21.2虚拟机。 1. 安装准备 1.1 安装平台 Windows 11 1.2. 软件信息 软件名称软件版本安装路径Oracle VM VirtualBoxVirtualBox-7.0.12-159484D:\softwareCentOS7CentOS-7.9.2009E:\…

4点优势,昂首资本使用浮动差价不使用固定差价的原因

在交易中&#xff0c;很多投资者和昂首资本一样&#xff0c;会使用浮动点差而不使用固定点差&#xff0c;那是因为投资者和昂首资本一样认为&#xff0c;使用浮动差价交易会比使用固定价差交易更有优势。 首先在大部分交易时段&#xff0c;价差缩小。正如投资者和昂首资本所知…