当前位置: 首页 > news >正文

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

相关文章:

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

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

剩余银饰的重量 - 华为OD统一考试

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 有N块二手市场收集的银饰&#xff0c;每块银饰的重量都是正整数&#xff0c;收集到的银饰会被熔化用于打造新的饰品。 每一回合&#xff0c;从中选出三块 最重的…...

redis远程连接不上解决办法

问题描述&#xff1a; redis远程服务端运行在192.168.3.90计算机上&#xff0c;客户端计算机&#xff08;ip:192.168.3.110&#xff09;通过redsi-cli.exe客户端工具连接时&#xff0c;没有反应&#xff0c;连接不上。 如图所示&#xff1a; 解决步骤&#xff1a; 步骤一&…...

利用Anaconda安装pytorch和paddle深度学习环境+pycharm安装后不能调用pytorch和paddlepaddle框架

问题现象&#xff1a; 之前安装后不能在添加pytorch和paddlepaddle框架 原因&#xff08;疑似&#xff09;&#xff1a; 在终端中显示pytorch和paddle在C盘但是安装是安装在J盘 解决办法&#xff1a; 卸载、删除文件重新安装后可以看到文件位置在J盘中 但是选择时还是显示C…...

Eclipses安装教程

一、下载开发工具包 1、开发工具包JDK 下载地址链接&#xff1a;https://www.oracle.com/cn/java/technologies/downloads/ 下载教程&#xff1a; 1&#xff09;点击链接&#xff0c;可以跳转到页面 2&#xff09;下滑页面&#xff0c;找到开发工具包 3&#xff09; 记住下载之…...

安装python版opencv的一些问题

安装python版opencv的一些问题 OpenCV是知名的开源计算机视觉算法库&#xff0c;提供了C\Python\Java版共享库。 在Python中使用OpenCV格外简单&#xff0c;一句命令就能安装&#xff0c;一行import就能引入&#xff0c;可谓是神器。然而&#xff0c;在实际使用中可能遇到一些…...

RabbitMQ入门实战

RabbitMQ 是一个开源的消息中间件&#xff0c;实现了高级消息队列协议&#xff08;AMQP&#xff09;&#xff0c;用于在分布式系统中进行消息传递。它能够在应用之间传递消息&#xff0c;解耦应用组件&#xff0c;提高系统的可伸缩性和可维护性。RabbitMQ 使用高级消息队列协议…...

vue3-模版引用ref

1. 介绍 概念&#xff1a;通过 ref标识 获取真实的 dom对象或者组件实例对象 2. 基本使用 实现步骤&#xff1a; 调用ref函数生成一个ref对象 通过ref标识绑定ref对象到标签 代码如下&#xff1a; 父组件&#xff1a; <script setup> import { onMounted, ref } …...

C# 十大排序算法

以下是常见的十大排序算法&#xff08;按照学习和实现的顺序排列&#xff09;&#xff1a; 冒泡排序&#xff08;Bubble Sort&#xff09;选择排序&#xff08;Selection Sort&#xff09;插入排序&#xff08;Insertion Sort&#xff09;希尔排序&#xff08;Shell Sort&…...

面试之Glide如何绑定Activity的生命周期

Glide绑定Activity生命周期 Glide.with() 下面都是它的重载方法&#xff0c;Context&#xff0c;Activity&#xff0c;FragmentActivity, Fragment, android.app.Fragment fragment,View都可以作为他的参数&#xff0c;内容大同小异&#xff0c;都是先getRetriever&#xff0…...

从 fatal 错误到 sync.Map:Go中 Map 的并发策略

为什么 Go 语言在多个 goroutine 同时访问和修改同一个 map 时&#xff0c;会报出 fatal 错误而不是 panic&#xff1f;我们该如何应对 map 的数据竞争问题呢&#xff1f; 这篇文章将带你一步步了解背后的原理&#xff0c;并引出解决 map 并发问题的方案。 Map 数据竞争 首先…...

Simon算法详解

0.0 Intro 相关的算法&#xff1a; Deutsh-Jozsa算法&#xff1a; 第一个量子算法对经典算法取得指数级加速的算法 美中不足在于只能确定函数是平衡的还是非平衡的&#xff0c;无法确定函数具体的内容&#xff0c;即无法直接解出函数 Bernstein-Vazirani算法&#xff…...

jrebel IDEA 热部署

1 下载 2022.4.1 JRebel and XRebel - IntelliJ IDEs Plugin | Marketplace 2 选择下载好的zip 离线安装IDEA 插件 重启IDEA 3 打开 [Preference -> JRebel & XRebel] 菜单&#xff0c;输入 GUID address 为 https://jrebel.qekang.com/1e67ec1b-122f-4708-87d…...

pdf拆分成各个小pdf的方法

背景:由于某些缘故,一个大的pdf需要拆分成页数少的pdf,或者pdf需要去掉指定页,那么就有必要对pdf进行重新编辑,这里需要用到一个库,直接进行操作即可。 当使用Python时,可以使用PyMuPDF库来拆分PDF文件。以下是一个示例代码, import fitz # PyMuPDF def split_pdf(i…...

IntelliJ IDEA 常用快捷键一览表(通用型,提高编写速度,类结构、查找和查看源码,替换与关闭,调整格式)

文章目录 IntelliJ IDEA 常用快捷键一览表1-IDEA的日常快捷键第1组&#xff1a;通用型第2组&#xff1a;提高编写速度&#xff08;上&#xff09;第3组&#xff1a;提高编写速度&#xff08;下&#xff09;第4组&#xff1a;类结构、查找和查看源码第5组&#xff1a;查找、替换…...

MSVS C# Matlab的混合编程系列2 - 构建一个复杂(含多个M文件)的动态库:

前言: 本节我们尝试将一个有很多函数和文件的Matlab算法文件集成到C#的项目里面。 本文缩语: MT = Matlab 问题提出: 1 我们有一个比较复杂的Matlab文件: 这个MATLAB的算法,写了很多的算法函数在其他的M文件里面,这样,前面博客的方法就不够用了。会报错: 解决办法如下…...

上位机图像处理和嵌入式模块部署(qt图像处理)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 很多人一想到图像处理&#xff0c;本能的第一反应就是opencv&#xff0c;这也没有错。但是呢&#xff0c;这里面还是有一个问题的&#xff0c;不知…...

AI教我学编程之C#类的实例化与访问修饰符

前言 在这篇文章中&#xff0c;我将带大家深入了解C#编程语言的核心概念&#xff0c;包括类的实例化、访问修饰符的应用&#xff0c;以及C#中不同数据类型的默认值。我会通过逐步分析和具体实例&#xff0c;详细解释如何在C#中正确创建和操作对象&#xff0c;并探讨如何通过访…...

【笔记】Blender4.0建模入门-3物体的基本操作

Blender入门 ——邵发 3.1 物体的移动 演示&#xff1a; 1、选中一个物体 2、选中移动工具 3、移动 - 沿坐标轴移动 - 在坐标平面内移动 - 自由移动&#xff08;不好控制&#xff09; 选中物体&#xff1a;右上的大纲窗口&#xff0c;点击物体名称&#xff0c;物体的轮…...

一文详解 Berachain 测试网:全面介绍与教程,bitget wallet教程

什么是Berachain&#xff1f; Berachain&#xff08;web3.bitget.com/zh-CN/assets/berachain-wallet&#xff09;是一种尖端区块链技术&#xff0c;使用 Cosmos SDK 构建的 Layer-1&#xff0c;兼容以太坊虚拟机&#xff08;EVM&#xff09;。它基于一种独特的概念&#xff0c…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析

今天聊的内容&#xff0c;我认为是AI开发里面非常重要的内容。它在AI开发里无处不在&#xff0c;当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗"&#xff0c;或者让翻译模型 "将这段合同翻译成商务日语" 时&#xff0c;输入的这句话就是 Prompt。…...

MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例

一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级

在互联网的快速发展中&#xff0c;高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司&#xff0c;近期做出了一个重大技术决策&#xff1a;弃用长期使用的 Nginx&#xff0c;转而采用其内部开发…...

华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建

华为云FlexusDeepSeek征文&#xff5c;DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色&#xff0c;华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型&#xff0c;能助力我们轻松驾驭 DeepSeek-V3/R1&#xff0c;本文中将分享如何…...

面向无人机海岸带生态系统监测的语义分割基准数据集

描述&#xff1a;海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而&#xff0c;目前该领域仍面临一个挑战&#xff0c;即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...

Oracle11g安装包

Oracle 11g安装包 适用于windows系统&#xff0c;64位 下载路径 oracle 11g 安装包...

32单片机——基本定时器

STM32F103有众多的定时器&#xff0c;其中包括2个基本定时器&#xff08;TIM6和TIM7&#xff09;、4个通用定时器&#xff08;TIM2~TIM5&#xff09;、2个高级控制定时器&#xff08;TIM1和TIM8&#xff09;&#xff0c;这些定时器彼此完全独立&#xff0c;不共享任何资源 1、定…...

基于单片机的宠物屋智能系统设计与实现(论文+源码)

本设计基于单片机的宠物屋智能系统核心是实现对宠物生活环境及状态的智能管理。系统以单片机为中枢&#xff0c;连接红外测温传感器&#xff0c;可实时精准捕捉宠物体温变化&#xff0c;以便及时发现健康异常&#xff1b;水位检测传感器时刻监测饮用水余量&#xff0c;防止宠物…...

【PX4飞控】mavros gps相关话题分析,经纬度海拔获取方法,卫星数锁定状态获取方法

使用 ROS1-Noetic 和 mavros v1.20.1&#xff0c; 携带经纬度海拔的话题主要有三个&#xff1a; /mavros/global_position/raw/fix/mavros/gpsstatus/gps1/raw/mavros/global_position/global 查看 mavros 源码&#xff0c;来分析他们的发布过程。发现前两个话题都对应了同一…...

表单设计器拖拽对象时添加属性

背景&#xff1a;因为项目需要。自写设计器。遇到的坑在此记录 使用的拖拽组件时vuedraggable。下面放上局部示例截图。 坑1。draggable标签在拖拽时可以获取到被拖拽的对象属性定义 要使用 :clone, 而不是clone。我想应该是因为draggable标签比较特。另外在使用**:clone时要将…...