第一个 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);		//初始化 OpenGLbool SetDCPixelFormat(HDC hDC);		//设置 DC 像素格式

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

bool COpenGLCubeDemoDlg::InitializeOpenGL(HDC hDC)
{//设置 DC 像素格式if (false == SetDCPixelFormat(hDC)){return false;}//创建 RCHGLRC 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 MappingglEnable(GL_DEPTH_TEST);								// Enables Depth TestingglDepthFunc(GL_LEQUAL);									// The Type Of Depth Testing To DoglHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);		// Really Nice Perspective Calculationsreturn true;
}
bool COpenGLCubeDemoDlg::SetDCPixelFormat(HDC hDC)
{static PIXELFORMATDESCRIPTOR pfd ={sizeof(PIXELFORMATDESCRIPTOR),	//pfd结构的大小1,								//版本号PFD_DRAW_TO_WINDOW |			//支持在窗口中绘图PFD_SUPPORT_OPENGL |			//支持OpenGLPFD_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)									//防止除0cy = 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 FaceglColor3ub(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 FaceglColor3ub(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 FaceglColor3ub(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 FaceglColor3ub(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 FaceglColor3ub(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 faceglColor3ub(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 FaceglTexCoord2i(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 FaceglTexCoord2i(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 FaceglTexCoord2i(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 FaceglTexCoord2i(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 FaceglTexCoord2i(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 faceglTexCoord2i(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 FaceglTexCoord2f(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 FaceglTexCoord2f(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 FaceglTexCoord2f(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 FaceglTexCoord2f(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 FaceglTexCoord2f(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 faceglTexCoord2f(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。

源码下载

  • https://download.csdn.net/download/blackwoodcliff/88737683

参考

  • 原生 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.rhkb.cn/news/239633.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

随笔03 笔记整理

图源&#xff1a;文心一言 关于我的考研与信息安全类博文整理~&#x1f95d;&#x1f95d; 第1版&#xff1a;整理考研类博文~&#x1f9e9;&#x1f9e9; 第2版&#xff1a;提前列出博文链接&#xff0c;以便小伙伴查阅~&#x1f9e9;&#x1f9e9; 第3版&#xff1a;整理We…

SLF4J Spring Boot日志框架

JAVA日志框架 JAVA有好多优秀的日志框架&#xff0c;比如log4j、log4j2、logback、JUL&#xff08;java.util.logging&#xff09;、JCL&#xff08;JAVA Common Logging&#xff09;等等&#xff0c;logback是后起之秀&#xff0c;是Spring Boot默认日志框架。 今天文章的目…

快速了解——逻辑回归及模型评估方法

一、逻辑回归 应用场景&#xff1a;解决二分类问题 1、sigmoid函数 1. 公式&#xff1a; 2. 作用&#xff1a;把 (-∞&#xff0c;∞) 映射到 (0&#xff0c; 1) 3. 数学性质&#xff1a;单调递增函数&#xff0c;拐点在x0&#xff0c;y0.5的位置 4. 导函数公式&#xff1a;f…

【镜像制作】OS云主机镜像的制作——以H3C为例

一、云主机镜像简介 1&#xff0e;云主机镜像 云主机镜像不同于容器镜像&#xff0c;是一个含有引导分区、操作系统以及必要应用的单一文件&#xff0c;可以理解成是对整个系统安装光盘所有数据的克隆文件。云用户在创建和申请云主机时可通过选择不同的镜像来快速获取相应操作…

一文读懂「Prompt Engineering」提示词工程

在了解提示过程之前&#xff0c;先了解一下什么是提示prompt&#xff0c;见最后附录部分 一、什么是Prompt Engingering&#xff1f; 提示工程&#xff08;Prompt Engingering&#xff09;&#xff0c;也被称为上下文提示&#xff08;In-Context Prompting&#xff09;&#x…

Android的setContentView流程

一.Activity里面的mWindow是啥 在ActivityThread的performLaunchActivity方法里面&#xff1a; private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {ActivityInfo aInfo r.activityInfo;if (r.packageInfo null) {r.packageInfo getP…

常见面试题之CSS

CSS3的新特性 新增选择器&#xff1a;:nth-child()、:first-of-type、:last-of-type等 弹性盒子&#xff1a;display: flex 媒体查询&#xff1a;media根据设备的特性和屏幕大小应用不同的样式规则 多列布局&#xff1a;column-count和column-with等属性可以实现将内容分为多…

SpringBoot读取配置文件中的内容

文章目录 1. 读取配置文件application.yml中内容的方法1.1 Environment1.2 Value注解1.3 ConfigurationProperties 注解1.4 PropertySources 注解&#xff0c;获取自定义配置文件中的内容&#xff0c;yml文件需要自行实现适配器1.5 YamlPropertiesFactoryBean 加载 YAML 文件1.…

缓存和数据库一致性

前言&#xff1a; 项目的难点是如何保证缓存和数据库的一致性。无论我们是先更新数据库&#xff0c;后更新缓存还是先更新数据库&#xff0c;然后删除缓存&#xff0c;在并发场景之下&#xff0c;仍然会存在数据不一致的情况&#xff08;也存在删除失败的情况&#xff0c;删除…

软件测试|解决Github port 443 : Timed out连接超时的问题

前言 GitHub是全球最大的开源代码托管平台之一&#xff0c;许多开发者和团队使用它来管理和协作开源项目。但在当下&#xff0c;我们在clone或者提交代码时会经常遇到"GitHub Port 443: Timed Out"错误&#xff0c;这意味着我们的电脑无法建立与GitHub服务器的安全连…

鸿蒙Harmony--AppStorage--应用全局的UI状态存储详解

无所求必满载而归&#xff0c;当你降低期待&#xff0c;降低欲望&#xff0c;往往会得到比较好的结果&#xff0c;把行动交给现在&#xff0c;用心甘情愿的态度&#xff0c;过随遇而安的生活&#xff0c;无论结果如何&#xff0c;都是一场惊喜的获得! 目录 一&#xff0c;定义 …

MySQL单表查询练习题

一、创建表的素材 表名&#xff1a;worker——表中字段均为中文&#xff0c;比如&#xff1a;部门号、工资、职工号、参加工作等 CREATE TABLE worker ( 部门号 int(11) NOT NULL, 职工号 int(11) NOT NULL, 工作时间 date NOT NULL, 工资 float(8,2) NOT NULL, 政治面貌 …

MySQL进阶45讲【1】基础架构:一条SQL查询语句是如何执行的?

1 前言 我们经常说&#xff0c;看一个事儿千万不要直接陷入细节里&#xff0c;应该先鸟瞰其全貌&#xff0c;这样能够帮助你从高维度理解问题。同样&#xff0c;对于MySQL的学习也是这样。平时我们使用数据库&#xff0c;看到的通常都是一个整体。比如&#xff0c;有个最简单的…

uniapp 字母索引列表插件(组件版) Ba-SortList

简介&#xff08;下载地址&#xff09; Ba-SortList 是一款字母索引列表组件版插件&#xff0c;可自定义样式&#xff0c;支持首字母字母检索、首字检索、搜索等等&#xff1b;支持点击事件。 支持首字母字母检索支持首字检索支持搜索支持点击事件支持长按事件支持在uniapp界…

八:分布式锁

1、为什么要使用分布式锁 锁是多线程代码中的概念&#xff0c;只有多任务访问同一个互斥的共享资源时才需要锁。单机应用开发时一般使用synchronized或lock。多线程的运行都是在同一个JVM之下。应用是分布式集群&#xff0c;属于多JVM的工作环境&#xff0c;JVM之间已经无法通过…

STM32 定时器输入捕获2——捕获高电平时长

由上图我们可以知道&#xff0c;高电平时间t2-t1。在代码中&#xff0c;可以记录此时t1的时间然后再记录t2的时间&#xff0c;t2-t1&#xff0c;就是我们所想要的答案。 但是&#xff0c;还有更简单一点点的&#xff0c;当到达t1的时候&#xff0c;我们把定时器清零&#xff0c…

MIT_线性代数笔记:第 26 讲 复矩阵;快速傅里叶变换

目录 复向量 Complex vectors复矩阵 Complex matrices傅里叶变换 Fourier transform快速傅里叶变换 Fast Fourier transform 实矩阵也可能有复特征值&#xff0c;因此无法避免在矩阵运算中碰到复数&#xff0c;本讲学习处理复数矩阵和复向量。 最重要的复矩阵是傅里叶矩阵&…

Linux CentOS 7.6安装JDK详细保姆级教程

一、检查系统是否自带jdk java --version 如果有的话&#xff0c;找到对应的文件删除 第一步&#xff1a;先查看Linux自带的JDK有几个&#xff0c;用命令&#xff1a; rpm -qa | grep -i java第二步:删除JDK&#xff0c;执行命令&#xff1a; rpm -qa | grep -i java | xarg…

【Nuxt3】Nuxt3脚手架nuxi安装项目和项目目录介绍

简言 最近学了Nuxt3,并使用它创建了自己的小网站。记录下学习到的nuxt3内容。 Nuxt3官网 Nuxt 是一个免费的开源框架&#xff0c;可通过直观、可扩展的方式使用 Vue.js 创建类型安全、高性能、生产级的全栈 Web 应用程序和网站。 支持SSR、SPA、建立静态网站&#xff0c;也可以…

【大数据】Flink 详解(九):SQL 篇 Ⅱ

《Flink 详解》系列&#xff08;已完结&#xff09;&#xff0c;共包含以下 10 10 10 篇文章&#xff1a; 【大数据】Flink 详解&#xff08;一&#xff09;&#xff1a;基础篇【大数据】Flink 详解&#xff08;二&#xff09;&#xff1a;核心篇 Ⅰ【大数据】Flink 详解&…