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

news2024/9/26 3:32:04

文章目录

  • 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) ->窗口,视口,裁剪区,视景体

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

R语言【paleobioDB】——pbdb_map():根据化石记录绘制地图

Package paleobioDB version 0.7.0 paleobioDB 包在2020年已经停止更新&#xff0c;该包依赖PBDB v1 API。 可以选择在Index of /src/contrib/Archive/paleobioDB (r-project.org)下载安装包后&#xff0c;执行本地安装。 Usage pbdb_map (data, col.int"white" ,p…

AI-图片转换中国风动漫人物

&#x1f3e1; 个人主页&#xff1a;IT贫道-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;私聊博主加WX好友&#xff0c;获取更多资料哦~ &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你学编程的个人空间-豹哥教你学编程个人主页-哔哩哔哩视频 目录 1. AI卡通秀原理 2. …

序章 搭建环境篇—准备战士的剑和盾

第一步&#xff1a;安装node.js Node.js 内置了npm&#xff0c;只要安装了node.js&#xff0c;就可以直接使用 npm&#xff0c;官网地址&#xff1a; Download | Node.js 在这里不建议安装最新版本的node.js&#xff0c;可以选跟我一样的版本&#xff0c;node版本v16.13.2 链…

Maven多模块项目打包:Unable to find main class

目录 一、错误来源 二、原始pom文件 common模块 pojo模块 server模块 父工程 三、解决方法 四、修改pom文件 common模块 pojo模块 server模块&#xff08;不改动&#xff09; 父工程 一、错误来源 使用Maven对项目进行多模块开发&#xff0c;在项目打包时出现错误…

Grind75第8天 | 278.第一个错误的版本、33.搜索旋转排序数组、981.基于时间的键值存储

278.第一个错误的版本 题目链接&#xff1a;https://leetcode.com/problems/first-bad-version 解法&#xff1a; 二分查找。 如果一个版本为错误版本&#xff08;isBadVersion为True&#xff09;&#xff0c;那么第一个错误版本在该版本左侧&#xff08;包括该版本&#x…

nova组件讲解和glance对接swift

1、openstack架构 &#xff08;1&#xff09;openstack是一种SOA架构&#xff08;微服务就是从这种架构中剥离出来的&#xff09; &#xff08;2&#xff09;这种SOA架构&#xff0c;就是把每个服务独立成一个组件&#xff0c;每个组件通过定义好的api接口进行互通 &#xff…

RK3568平台 温度传感器芯片SD5075

一.SD5075芯片简介 SD5075 是一款高准确度温度传感器芯片内含高精度测温 ADC&#xff0c;在-40C ~100C 范围内典型误差小于0.5C&#xff0c;在-55C~125C 范围内典型误差小于士1.0C。通过两线 IC/SMBus接口可以很方便与其他设备建立通信。设置 A2~A0 的地址线&#xff0c;可支持…

如何使用“通义听悟”提高工作和学习效率

如何使用通义听悟提高工作和学习效率 通义听悟是一款利用人工智能技术&#xff0c;自动为音频和视频内容提供转写、翻译、总结、检索等功能的在线工具。它可以在会议、学习、访谈、培训等场景下&#xff0c;帮助您记录、阅读、整理、复习音视频信息&#xff0c;成为您的工作和…

深入浅出线程原理

Linux 中的线程本质 线程接口由 Native POSIX Thread Library 提供&#xff0c;即&#xff1a;NPTL 库函数 线程被称为轻量级进程 (Light Weight Process) 每一个线程在内核中都对应一个调度实体&#xff0c;拥有独立的结构体 (task_struct) 内核设计&#xff1a;一个进程对…

python + selenium 初步实现数据驱动

如果在进行自动化测试的时候将测试数据写在代码中&#xff0c;若测试数据有变&#xff0c;不利于数据的修改和维护。但可以尝试通过将测试数据放到excel文档中来实现测试数据的管理。 示例&#xff1a;本次涉及的项目使用的12306 selenium 重构------三层架构 excel文件数据如…

【RV1126 学习】SDK/ U-Boot/kernel/rootfs 编译学习

文章目录 RV1126芯片介绍rv1126 模块代码目录相关说明 SDK 包下的脚本使用build.sh 脚本使用envsetup.sh 脚本使用mkfirmware.sh 脚本使用rkflash.sh 脚本使用 U-Boot 编译和配置uboot 的配置修改编译操作 kernel 的修改编译rootfs 编译和配置buildroot 配置busybox 配置 RV112…

如何从电脑找回/恢复误删除的照片

按 Shift Delete 以后会后悔吗&#xff1f;想要恢复已删除的照片吗&#xff1f;好吧&#xff0c;如果是这样的话&#xff0c;那么您来对地方了。在本文中&#xff0c;我们将讨论如何从 PC 中检索已删除的文件。 自从摄影的概念被曝光以来&#xff0c;人们就对它着迷。早期的照…

SQL:一行中存在任一指标就显示出来

当想要统计的两个指标不在一张表中时&#xff0c;需要做关联。但很多情况下&#xff0c;也没有办法保证其中一张表的维度是全的&#xff0c;用left join或right join可能会导致数据丢失。所以借助full join处理。 如&#xff0c;将下面的数据处理成表格中的效果&#xff08;维…

IntersectionObserver

IntersectionObserver 这个API主要实现图片懒加载、加载更多等等。 该API作用是观察两个元素之间有没有交叉&#xff0c;有没有重叠 现在要做的是当图片跟视口有交叉的情况下&#xff0c;把data-src的图片路径替换给src属性 //第一个参数是 回调&#xff0c;第二个参数的 配置…

论文翻译 | 【深入挖掘Java技术】「底层原理专题」深入分析一下并发编程之父Doug Lea的纽约州立大学的ForkJoin框架的本质和原理

深入分析一下并发编程之父Doug Lea的纽约州立大学的ForkJoin框架的本质和原理这里写目录标题 前提介绍摘要引言设计性能优秀任务粒度合理Cilk框架和基础fork/join的可移植性FJTask框架设计思路线程映射关系拆分子任务排队及调度设置调度管理 标准示例 未完待续 前提介绍 Doug …

【linux驱动开发】在linux内核中注册一个杂项设备与字符设备以及内核传参的详细教程

文章目录 注册杂项设备驱动模块传参注册字符设备 开发环境&#xff1a; windows ubuntu18.04 迅为rk3568开发板 注册杂项设备 相较于字符设备&#xff0c;杂项设备有以下两个优点: 节省主设备号:杂项设备的主设备号固定为 10&#xff0c;在系统中注册多个 misc 设备驱动时&…

c++学习笔记-STL案例-机房预约系统2-创建身份类

前言 衔接上一篇“c学习笔记-STL案例-机房预约系统1-准备工作”&#xff0c;本文主要包括&#xff1a;创建身份类&#xff0c;建立了整个系统的框架&#xff0c;Identity基类&#xff0c;派生类&#xff1a;Sudent、Teacher、Manager&#xff0c;基类无实现源文件&#xff0c;…

旺店通·企业版和金蝶云星空接口打通对接实战

旺店通企业版和金蝶云星空接口打通对接实战 对接源平台:旺店通企业版 慧策&#xff08;原旺店通&#xff09;是一家技术驱动型智能零售服务商&#xff0c;基于云计算PaaS、SaaS模式&#xff0c;以一体化智能零售解决方案&#xff0c;帮助零售企业数字化智能化升级&#xff0c;实…

【python】matplotlib画图常用功能汇总

目录: 一、matplotlib画图风格二、matplotlib图像尺寸和保存分辨率三、matplotlib子图相关功能创建子图&#xff1a;绘制子图&#xff1a;设置子图属性&#xff1a;调整布局&#xff1a;示例代码&#xff1a; 四、matplotlib字体设置字体族和字体的区别字体选择和设置1. Matplo…

蓝桥杯单片机组备赛——数码管静态显示

文章目录 一、预备知识1.1 段选&位选1.2 静态显示和动态显示 二、原理图分析三、代码编写思路四、最终代码五、静态数码管显示练习与答案5.1 练习题目5.2 练习答案 一、预备知识 1.1 段选&位选 段选&#xff1a;段选指的是选择数码管哪个“段”应该被点亮。一般情况下…