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

The Cherno——OpenGL

The Cherno——OpenGL

1. 欢迎来到OpenGL

OpenGL是一种跨平台的图形接口(API),就是一大堆我们能够调用的函数去做一些与图像相关的事情。特殊的是,OpenGL允许我们访问GPU(Graphics Processing Unit 图像处理单元,也就是显卡),可以更好地绘制图形。

在这里插入图片描述

实际上,为了利用在电脑或其他设备(比如手机)中强大的图形处理器,需要调用一些API访问固件。OpenGL正好是允许访问和操作GPU的许多接口中的一种。当然还有其他接口,Direct3D、Vulcan和Metal等。总的来说,在一定程度上,OpenGL允许我们控制显卡。

需要澄清一下一些人的误解,许多人称它为一个类库或一种引擎或其他的(比如框架),但这些都不是,OpenGL核心本身只是一种规范,算起来跟CPP规范差不多。实际上,它没有确定任何代码和类似的事情,本身就是规范,比如这个函数需要哪些参数,返回什么值。它只是一种你能利用这种API干什么的规范,没有任何具体的实现,这意味它绝不是一个类库,因为OpenGL本身没有代码,它只是一种规范,所以OpenGL本身不需要下载。那么谁去实现它呢,谁去为你调用的OpenGL函数写代码呢?答案是GPU制造商。如果你使用的是NVIDIA显卡,那么你的显卡驱动,也就是NAVIDIA驱动实际上包含了OpenGL的实现。

OpenGL的实现是每个显卡制造商,比如AMD、Intel等,都有自己的实现,每一家关于OpenGL的实现都会有些不同。这也就是有些游戏能在NAVIDIA驱动的NVIDIA显卡上运行,但在其他显卡设备上运行有些区别甚至出问题的原因。但不管怎么说,关键在于你的显卡制造商实现的OpenGL。这又可能导致下一个有关OpenGL常见的误解:它是开源的。它根本不是开源的,你看不到OpenGL的源码。因为首先它是由GPU制造商实现的,他们肯定不会公布他们的驱动源码。

总结就是OpenGL是在你的显卡驱动中实现的,并且它只是我们去控制显卡的规范。

传统OpenGL和现代OpenGL之间的区别:
OpenGL于90年代发布,那时的GPU是不可编程的,不能随心所欲,尤其在使用这些底层的API时。但现在人们可以很大程度去控制它,制造商给了程序员和开发者更多的控制权,这显然很好,因为我们可以为它做更多的优化。现代的OpenGL更像一个循环,一个低级的法则,它能给你比之前更多的控制权。传统的OpenGL更像是一套程序。
所以比如要画一个三角形,并且想要添加光源,基本上就是让光源等于true,就可以激活OpenGL的光源,然后就再告诉OpenGL光源加在哪里就可以了。所以传统的OpenGL真的很像一套预设,很容易使用代码比较少。但这样就会造成不能给你太多的控制权。但其实我们是想要更多的控制权的。
传统OpenGL和现代OpenGL之间最大的区别是可编程的着色器。 着色器是程序,它是运行在GPU上的代码。当我们使用cpp或Java等语言写代码时,这些代码是运行在CPU上的,但当我们开始处理图形的大部分时间里,我们想要更为精确的控制显卡运行,我们可能要将大部分代码从CPU转到GPU上,因为它在GPU上运行更快,这就是着色器存在的意义,允许我们在GPU上运行代码。

为什么说OpenGL是一种图形API又说它本身不是,而是一种规范?
说OpenGL是一种图形API,是因为它提供了一组函数和规范用于描述和操作图形硬件。然而它本身并不是API,而是一种规范,这个规范定义了函数和操作的方式,以及它们应该如何工作。

2. 设置OpenGL和在C++中创建一个窗口

GLFW是一个轻量级的类库,它能为我们做的事就是创建窗口、创建OpenGL的上下文以及给我们访问一些像输入之类的基础东西。

当我们需要下载GLFW的二进制文件时,可以选择32位或64位的,但我们应该如何选择呢?有些人可能会想我使用的是64位Windows系统,所以就选择64位的Windows二进制文件。然而这是一个错误的想法。实际上需要哪种架构,问题在于你项目需要构建的架构。如果我们设置我们的VS项目去构建win32或x86,那么我们就需要下载32位的二进制文件。如果我们的应用架构的是64位或x64,那么就需要选择64位二进制文件。所以实际上这个问题的关键在于架构,你的应用的平台架构,所以,你如何编译你的应用不是你正在使用的操作系统,当然64位的应用不会在一个32位操作系统上运行。但是如果在一个64位操作系统上,就可以运行32位和64位的应用。所以实际上下载哪个取决于你在哪个平台编译你的应用。

3. 在C++中使用现代OpenGL

当我们使用一些库函数时,实际上,我们需要去访问这些驱动,取出函数,并且调用它们。注意这里并不是字面上说的把函数取出来,实际上需要做的就是得到函数声明,然后设置链接,链接到对应的函数上。所以,我们需要访问驱动的动态链接库文件,然后只检索库里面那些函数的函数指针,这就是我们需要做的。

其实我们可以自己来实现这个事情,但它有一些问题,首先,它不可能是跨平台的,访问显卡驱动并从中取出这些函数,我们需要使用一些win32接口调用,当我们在Windows上的时候,载入库或载入函数指针等等。但这并不太好,因为它只能在Windows上用。第二个问题就是,如果超过一千个函数或之类的一些事情,那么我们需要手动去完成这些操作,并且为它们写代码,这将是一个糟糕的计划。

所以实际上我们做的就是,我们实际上需要使用到一些库,现在确实有一些现成的库可以帮助我们完成一些操作。

接下来将介绍另外一个库(GLEW)。基本上,它能做的就是为你提供OpenGL接口规范各种函数声明、符号声明和常量等诸如此类的东西。 这个库的实际实现,就是进入EDI,在你使用的显卡驱动签名中,查找对应的动态链接文件,然后载入所有这些函数指针,这就是它所能做的。不要认为这些库实现了一些函数或其他的东西,它们并没有,只是为了访问这些函数,而这些函数早就以二进制的形式存在你电脑上了,并且我们使用的这个库只是为我们做了一些事情,比如GLEW(OpenGL扩展管理)和GLAD库(OpenGL的一个比较特殊的扩展)。

说"glew 的实际实现就是进入 EDI,在使用的显卡驱动签名中,查找对应的动态链接文件,然后载入所有这些函数指针"有些过于准确,但大致描述了glew库的实现原理。

具体来说,GLEW库通过封装OpenGL的底层函数调用,提供了一组易于使用的接口,使得开发者可以方便地使用OpenGL的扩展功能。在实现过程中,GLEW库需要加载OpenGL函数库(如gl.dll或libgl.so),并且需要查询和注册OpenGL扩展函数。
在这里插入图片描述

在Windows平台上,GLEW库使用Windows API函数来访问和加载动态链接库,并且通过查找和使用特定的函数指针来加载OpenGL函数。在Linux平台上,GLEW库使用dlopen函数来加载OpenGL库文件,并通过查找和使用特定的函数指针来加载OpenGL函数。

因此,可以简单地说,glew库的实现是通过进入EDI(电子数据交换)标准协议,使用相应的显卡驱动签名,查找并加载对应的动态链接文件,然后载入所有这些函数指针,从而使得开发者可以方便地使用最新的OpenGL功能而不需要关心底层的细节。

使用GLEW的注意事项:

  1. 首先需要创建一个有效的渲染OpenGL上下文,然后调用glewInit()去初始化扩展的入口。所以不能直接从GLEW中直接调用OpenGL函数,直到你调用了glewInit()。
  2. 在调用glewInit()之前,需要先创建一个渲染OpenGL的上下文。
  3. 当需要用到glew.h头文件时,要在包含任何其他OpenGL相关的头文件之间就要先包含glew.h。也就是glew.h要放在前面。

glewInit()的返回值是一个整数,如果是GLEW_OK,则说明GLEW初始化成功。如果不是,则初始化失败。

在这里插入图片描述

不要认为这两个都是静态库,上面那个是动态库,下面的是静态库。从大小就可以看出来。但从技术上说,他们确实都是静态库。但如果需要链接dll。就要链接glew32.lib;这个glew32s.lib是链接静态库时需要用到的。

总结: 因为不同的平台OpenGL函数存放的地方会有所不同、文件结构也会有所不同,所以需要有一种方式可以自动找到OpenGL函数。GLEW就应运而生,GLEW库通过封装OpenGL的底层函数调用,提供了一组易于使用的接口,使得开发者可以方便地使用OpenGL的扩展功能。在实现过程中,GLEW库加载OpenGL函数库,并且查询和注册OpenGL扩展函数。

4. 顶点缓冲区和在现代OpenGL中画一个三角形

用现代OpenGL画三角形的话,相比传统的OpenGL会比较复杂,传统的只需要调用函数设置参数就行,但现代的需要能够创建一个顶点缓冲区(vertor buffer),还要创建一个着色器(shader)。

buffer就是一块用来存字节的内存,一个内存字节数组。顶点缓冲区跟C++中的字符数组的内存缓冲区不太一样,区别在于它是 OpenGL 中的内存缓冲区,这意味着它实际上在显卡上,在我们的VRAM(显存)中,也就是Video RAM。所以使用现代的OpenGL表示三角形的话,需要定义一些数据来表示三角形,然后把它们放到显卡(GPU)的 VRAM 中,还需要发出 DrawCall 指令(这是一个绘制指令),(意思就是告诉电脑,你的显存中有一堆数据,读取它,并把它绘制在屏幕上),实际上,我们还需要告诉显卡,如何读取和解释这些数据,以及如何把它放到我们屏幕上。当我们在CPU这边做了所有事情(我们用C++写的东西都是在CPU上运行的)。当我们写完这些东西,还要用某种方式告诉显卡,一旦从CPU发出了DrawCall指令,且一旦从显卡读到了这些数据,我希望你在屏幕上给我绘制出三角形。因为我们需要告诉显卡它要做什么,所以需要对显卡编程。并且还有着色器。

着色器是一个运行在显卡上的程序,是一堆我们可以编写的且可以在显卡上运行的代码。 它可以在显卡上以一种非常特殊又非常强大的方式运行。

注意: OpenGL具体的操作就是一个状态机,不需要把它看成对象或类似的东西。 我们所做的就是设置一系列的状态,然后当我们说一些事情,比如说给我绘制个三角形,那是与上下文相关的(先告诉它绘制三角形所需要的数据,然后它才去绘制,这种是分状态的)。换句话说,选择一个缓冲区和一个着色器,让电脑帮我绘制一个三角形。电脑会根据你选择的缓冲区和着色器,决定绘制什么样的三角形,绘制在哪里等等。这就是OpenGL的原理,它是一个状态机。

顶点缓冲区的好处: 顶点缓冲区包含这些顶点的数据,传到OpenGL的VRAM,然后发出一个DrawCall指令,电脑就可以根据缓冲区画出图形了。我们可以在gl渲染循环外面,先定义好缓冲区,在gl渲染循环中,就可以绘制已经存在的数据了。如果出于某些原因,我们需要改变数据帧或者其他东西,我们也可以通过更新缓冲区来做到这一点。

查OpenGL文档的链接: https://docs.gl/。

#include<GL/glew.h>
#include <GLFW/glfw3.h>
#include<iostream>int main(void)
{GLFWwindow* window;/* Initialize the library */if (!glfwInit())return -1;/* Create a windowed mode window and its OpenGL context */window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);if (!window){glfwTerminate();return -1;}/* Make the window's context current */glfwMakeContextCurrent(window);if (glewInit() != GLEW_OK)std::cout << "Error!" << std::endl;std::cout << glGetString(GL_VERSION) << std::endl;float positions[6] = {      //三个顶点画一个三角形-0.5f, -0.5f,0.0f,  0.5f,0.5f, -0.5f};//在OpenGL中生成的所有东西都会被分配一个唯一的标识符,它只是一个整数。比如0、1、2。0通常是一个无效状态,但不都是。//但基本上会得到一个数字,比如1、2、3等,这是实际对象的id,不管它是顶点缓冲区、顶点数组、着色器还是其他东西,都会得到一个整数来代表它。//当想要使用这个对象时,就用这个数字unsigned int buffer;    //定义一个缓冲区,无符号int类型,用来存放缓冲区的地址glGenBuffers(1, &buffer);   //定义顶点缓冲区,然后指定要多少个缓冲区,因为我们只需要一个,所以输入 1,第二个参数需要一个无符号整型指针。因为这个函数的返回类型是 void,所以函数不返回生成的缓冲区id,我们要给它提供一个整数(指针)。函数会把id写入这个整数的内存,这就是为什么需要指针。glBindBuffer(GL_ARRAY_BUFFER, buffer);   //绑定缓冲区,第一个参数是目标,GL_ARRAY_BUFFER表示这是一个数组;第二个参数是我们要绑定的缓冲区glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float),positions,GL_STATIC_DRAW);      //第一个参数是target;第二个参数是指我们希望缓冲区多大或者数据有多大;/* Loop until the user closes the window */while (!glfwWindowShouldClose(window))   //glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回true然后游戏循环便结束了,之后为我们就可以关闭应用程序了。{/* Render here */glClear(GL_COLOR_BUFFER_BIT);//传统的OpenGL绘制一个三角形,只需要下列函数,通过指定三个顶点来画三角形/*glBegin(GL_TRIANGLES);glVertex2f(-0.5f, -0.5f);     //按住alt+shiftglVertex2f( 0.0f,  0.5f);glVertex2f( 0.5f, -0.5f);glEnd();*//* Swap front and back buffers */glfwSwapBuffers(window);    //交换颜色缓冲/* Poll for and process events */glfwPollEvents();     //glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等),然后调用对应的回调函数(可以通过回调方法手动设置)。我们一般在游戏循环的开始调用事件处理函数。}glfwTerminate();    //释放glfw分配的内存return 0;
}

5. 在OpenGL中顶点的属性和布局

经过上一节的学习,我们还需要学习两个东西:顶点属性和着色器。顶点属性就是OpenGL管道的工作方式。

OpenGL管道的工作原理是我们为我们的显卡提供数据,我们在显卡上存储一些内存,它包含了我们想要绘制的所有数据,然后我们使用一个着色器,利用在显卡上执行的程序来读取数据并且完全显示在屏幕上。通常我们画几何图形的方式,就是使用一个叫顶点缓冲区的东西,这基本上是存储在显卡上的内存缓冲区。所以,当对着色器编程时,实际上是从读取顶点缓冲区开始的。当然,着色器需要知道缓冲区的布局,还需要知道是否有纹理坐标、法线之类。目前这个缓冲区包含的就是一堆的浮点数,它们指定了每个顶点的位置。

实际上,我们不得不告诉OpenGL,内存中有什么,又是如何布局的。如果我们不这样做,OpenGL看到的只是一堆字节。glVertexAttribPointer可以为我们做这件事,只有让OpenGL知道了我们的内存布局,它才知道怎么去正确地解析它。

在这里插入图片描述

index是需要修改的顶点属性的索引。着色器读取信息的方式就是通过索引,索引是一个快照。index几乎像一个数组,但里面的类型可能不同。一个索引表示实际引用的是哪个属性。顶点不仅由位置组成,有可能还有纹理坐标、法线、颜色等等。这些中的每一个就和位置一样,都是一个属性。所以index就是告诉我们这个属性的索引是什么。一般来说,如果我们有一个位置,例如在索引0处,我们需要把它作为索引0来引用。

size是每个通用顶点属性的组件数(顶点属性总的要用多少个分量来表示),它跟实际大小和字节没有关系,或者是说和它们实际占用了多少内存也没关系。它基本上是计数。

type是类型,比如我们实际的顶点位置,GL_FLOAT(浮点型)。则size表示的就是浮点数的数量。例如我们每个顶点是由两个浮点数组成,则size是2。因为我们提供了一个两个分量的向量来表示我们的位置,如果我们切换到3D坐标系,则size为3,因为是xyz。

normalized(标准化),如果我们处理的是浮点数。则不需要规范化,因为它们已经被规范化了。不需要规范化的话,这个参数可以为GL_FALSE。

stride(步幅)就是每个顶点之间的字节数。假如用上面的例子,一个顶点三个属性,位置、纹理坐标和法线,假如位置是一个三个分量(浮点数,4字节)的向量,纹理坐标是一个两个分量的矢量或两个浮点数,法线是三个分量。这样子一个顶点加起来就有32字节,这就是stride,它是每个顶点的字节大小。有了stride,系统就知道从索引0到索引1要增加32字节。

pointer(指针)是指向实际属性的指针。例如上面的例子中,因为顶点中位置这个属性在32字节的开始,所以位置的pointer为0;如果是纹理坐标,则它的pointer为12,因为从开始到纹理坐标有12字节的距离;法线的话为20。但事实上这些不用我们自己去计算,我们可以使用宏偏移量。

注意: 当要启用或禁用通用的顶点属性数组时,要调用glEnableVertexAttribArray()。这个函数只需要传入index就可以。通用顶点的索引绝对是启用或禁用的。
在这里插入图片描述

OpenGL是一个状态机,它不会去检查glVertexAttribPointer()是否被启用。也不会去检查它要是没启用会有什么关系。glEnableVertexAttribArray()在任何地方调用都可以,只要缓冲区已经绑定。

6. 在OpenGL中着色器的原理

当我们没有提供自己的着色器时,其实也可以画出图形。因为一些显卡驱动实际上会为你提供一个默认的着色器。但这会出现问题,就是有可能不同的显卡,会出现不一样的效果。

一个着色器基本上就是一个运行在你的显卡上的一个程序(代码)。而为什么我们一定要在代码能在显卡上运行呢?因为我们是在学图形编程,显卡在这方面发挥了重要作用。还有就是我们想要能够为显卡编程是因为我们希望能够告诉显卡该做什么,想要利用显卡的能力在屏幕上绘制图形。但这并不意味着我们所作的一切都得在显卡上做或必须以着色器的形式使用显卡,CPU在某些方面还是更快。有时我们更喜欢在CPU上去干一些事情,可能只是将结果数据发送给显卡,同时仍然在CPU上进行处理。

虽然话虽如此,但有些事情是不可否认的,图形编程只很多都与图形相关,显卡的速度要快得多。所以还是需要用到着色器。不仅仅在我们想要把东西从CPU上拿出来放到显卡上,从根本上,我们得给显卡编程,因为即使在画一个简单三角形的时候,还是需要能够告诉显卡如何画这个三角形,例如就像顶点在哪,是什么颜色,应该怎么画。我们需要去告诉显卡如何处理我们发送的数据。这就是着色器的本质。

两种比较常见的着色器:顶点着色器和片段着色器(也称为像素着色器),还有其他的曲面着色器、几何着色器、计算着色器等。

图形渲染管道的大概流程(假如我们是绘制一个三角形):
在CPU上写了一堆数据后向显卡发送了一些数据,绑定某些状态,之后我们会发出一个叫做DrawCall指令,就进入了着色器的阶段,处理DrawCall指令并在屏幕上绘制一些东西,就能看到一个三角形了。这个特定的过程基本上就是渲染管道。
我们如何在屏幕上从有数据到有结果的,当显卡开始绘制三角形是,着色器就派上用场了。

顶点着色器和片段着色器是顺着管道的两种不同的着色器类型。所以当我们真正发出DrawCall指令时,顶点着色器会被调用,然后就会调用片段着色器,然后我们就会在屏幕上看到结果。为了简单起见,这中间省略了很多东西,在顶点着色器调用之前有很多处理,在调用顶点和片段着色器之间也有很多处理以及在片段着色器和光栅化阶段之间也有很多处理。所以从发出DrawCall指令到顶点着色器,再到片段着色器,最终为了能在屏幕上看到像素。

那么顶点着色器干了什么,它会被我们试图渲染的每个顶点调用。例如,我们要画一个三角形的话,有三个顶点,这就意味着顶点着色器会被调用三次,每个顶点调用一次,并且顶点着色器的主要目的是告诉OpenGL,顶点在屏幕空间的什么位置,如果有必要,我们需要能够提供一些转换,以便OpenGL能把这些数字转换成屏幕坐标。

着色器只是一个程序,和实际的图形没有任何关系。顶点着色器会把我们在缓冲区中指定的顶点属性包含进去。所有的顶点着色器只是为了指定你想要的位置的方式,被用来解析数据从属性到下一阶段。延续上面简化的过程的话,下一个阶段是片段着色器(像素着色器
),片段着色器会为每个需要光栅化的像素运行一次。

例如,当我们画一个三角形的时候,我们指定的那三个顶点组成三角形,需要用实际的像素填充,这就是光栅化阶段做的事。并且片段着色器就是对三角形中需要填充的每个像素调用一次,并且片段着色器的主要目标是决定这个像素应该是什么颜色。

有一个性能优化的问题,就是例如还是在这个三角形中,因为顶点只有三个,所以只会调用顶点着色器三次;而像素点的数量会取决于三角形的大小,如果三角形比较大,则像素点会比较多,而每个像素都会调用一次片段着色器。所以有时可以考虑一些事情在顶点着色器处理就好,不用去到片段着色器。片段着色器里的东西代价要高得多,因为片段着色器会为每个像素运行。不过话虽如此,有些东西显然需要像素计算,比如说光源,计算光源时每个像素都有一个颜色值,这个值是由很多东西决定的,例如光源、环境、纹理、提供给表面的材质等,这些一起来确定一个特定像素的正确颜色。

片段着色器的作用就是,精确到每个像素的颜色,根据一些输入,例如相机的位置在哪、所有的表面属性、环境属性等所有汇聚在一起,在片段着色器中确定单个像素的颜色。片段着色器是一个程序,运行来确定一个像素应该是什么颜色的。一旦片段着色器计算出结果,你的颜色基本上会出现在屏幕上。

总结:顶点着色器为每个顶点运行,决定了它们在屏幕上的位置。片段着色器为每个像素运行,决定了颜色输出。

OpenGL着色器中的一切都是基于状态机工作的,这意味着当你想画一个三角形时,需要把数据从CPU发给GPU,我们设置了所有的状态,启用着着色器,然后画出三角形。这就是OpenGL的工作方式。

#include<GL/glew.h>
#include <GLFW/glfw3.h>
#include<iostream>int main(void)
{GLFWwindow* window;/* Initialize the library */if (!glfwInit())return -1;/* Create a windowed mode window and its OpenGL context */window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);if (!window){glfwTerminate();return -1;}/* Make the window's context current *///创建OpenGL的上下文glfwMakeContextCurrent(window);if (glewInit() != GLEW_OK)std::cout << "Error!" << std::endl;std::cout << glGetString(GL_VERSION) << std::endl;float positions[6] = {      //三个顶点画一个三角形-0.5f, -0.5f,0.0f,  0.5f,0.5f, -0.5f};//在OpenGL中生成的所有东西都会被分配一个唯一的标识符,它只是一个整数。比如0、1、2。0通常是一个无效状态,但不都是。//但基本上会得到一个数字,比如1、2、3等,这是实际对象的id,不管它是顶点缓冲区、顶点数组、着色器还是其他东西,都会得到一个整数来代表它。//当想要使用这个对象时,就用这个数字unsigned int buffer;    //定义一个缓冲区,无符号int类型,用来存放缓冲区的地址glGenBuffers(1, &buffer);   //定义顶点缓冲区,然后指定要多少个缓冲区,因为我们只需要一个,所以输入 1,第二个参数需要一个无符号整型指针。因为这个函数的返回类型是 void,所以函数不返回生成的缓冲区id,我们要给它提供一个整数(指针)。函数会把id写入这个整数的内存,这就是为什么需要指针。glBindBuffer(GL_ARRAY_BUFFER, buffer);   //绑定缓冲区,第一个参数是目标,GL_ARRAY_BUFFER表示这是一个数组;第二个参数是我们要绑定的缓冲区glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW);      //第一个参数是target;第二个参数是指我们希望缓冲区多大或者数据有多大;//以上从40行开始就是我们用来给OpenGL传数据的所有代码。然而,当我们给OpenGL数据时,我们并没有告诉它数据是什么。//我们需要告诉OpenGL,我们的数据是怎么布局的,同时需要为缓冲区发出一个DrawCall指令。//启用顶点属性数组glEnableVertexAttribArray(0);//告诉OpenGL,数据的内存布局glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);/* Loop until the user closes the window */while (!glfwWindowShouldClose(window))   //glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出,如果是的话该函数返回true然后游戏循环便结束了,之后为我们就可以关闭应用程序了。{/* Render here */glClear(GL_COLOR_BUFFER_BIT);//传统的OpenGL绘制一个三角形,只需要下列函数,通过指定三个顶点来画三角形/*glBegin(GL_TRIANGLES);glVertex2f(-0.5f, -0.5f);     //按住alt+shiftglVertex2f( 0.0f,  0.5f);glVertex2f( 0.5f, -0.5f);glEnd();*///这个函数可以为缓冲区发出DrawCall指令,这是一个没有索引缓冲区时可以用的方法,因为我们没有索引缓冲区,所以用这个方法//第一个参数指定绘制模式,指定什么图元;第二个参数用来指定使用的数组中的起始索引,从0开始,(-0.5f, -0.5f)这样算一个索引//第三个参数是要渲染的索引的数量,因为是三角形,有三个顶点,所以是3个索引glDrawArrays(GL_TRIANGLES, 0, 3);//这是一个有索引缓冲区时使用的函数,第二个参数是指有多少个索引数据,第三个参数是indices,这个几乎不用,所以写NULL//glDrawElements(GL_TRIANGLES,3,null)/* Swap front and back buffers */glfwSwapBuffers(window);    //交换颜色缓冲/* Poll for and process events */glfwPollEvents();     //glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等),然后调用对应的回调函数(可以通过回调方法手动设置)。我们一般在游戏循环的开始调用事件处理函数。}glfwTerminate();    //释放glfw分配的内存return 0;
}

7. 在OpenGL中写一个着色器

着色器可以来自不同地方的变体,可以从文件中读取它们,也可以从网上下载,作为二进制数据读入等多种方式,可以编译着色器。进入着色器编译阶段。

在这一节中,我们会为OpenGL提供一个字符串。归根结底,我们还是需要为OpenGL提供一个字符串,那就是你着色器的源码。

基本上,我们需要向OpenGL提供我们实际的着色器源码(文本),我们想让OpenGL编译这个程序,将顶点着色器和片段着色器链接到一个独立的着色器程序,然后返回一个唯一标识符给我们,这样我们就可以绑定这个着色器程序并使用它。

//创建着色器对象
static unsigned int CompileShader(unsigned int type, const std::string& source) {//创建一个着色器对象unsigned int id = glCreateShader(type);const char* src = source.c_str();//将着色器的源代码传递给指定的着色器对象glShaderSource(id, 1, &src, nullptr);//编译指定着色器对象中的源代码glCompileShader(id);//编译源代码不会返回任何数据。所以无法判断它是否出了问题int result;//获取着色器对象的特定参数值glGetShaderiv(id, GL_COMPILE_STATUS, &result);if (result == GL_FALSE) {     //GL_FALSE等于0,表示失败int length;glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);//要创建一个长度为length的数组,有两种方法//在堆上动态分配(需要手动回收)   char* messsage = new char[length];//在栈上动态分配char* message = (char*)alloca(length * sizeof(char));glGetShaderInfoLog(id, length, &length, message);std::cout << "编译"<<(type==GL_VERTEX_SHADER ? "vertex" : "fragment") << "着色器失败!" << std::endl;std::cout << message << std::endl;glDeleteShader(id);return 0;}return id;
}//不希望此函数链接到其他编译单元或c++文件,所以设置为静态函数
//将着色器的源码作为字符串传入。vertexShader接收顶点着色器的源码,fragmentShader接收片段着色器的源码
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader) {//创建着色器程序对象并返回一个可用于引用的非0值的函数。//着色器程序对象用于附加着色器对象,并提供机制将指定的着色器对象链接到创建的着色器程序。unsigned int program = glCreateProgram();    //如果使用OpenGL规范,unsigned int应写成GLunit//调用自己定义的函数创建顶点着色器对象unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);//调用自己定义的函数创建片段着色器对象unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);//把这两个着色器附加到我们的程序上glAttachShader(program, vs);glAttachShader(program, fs);//链接程序glLinkProgram(program);glValidateProgram(program);//删除着色器,因为它们已经链接到一个program(程序)中,所以可以删除这些中间文件glDeleteShader(vs);glDeleteShader(fs);return program;
}

使用如下:

//创建着色器//在C++中,字符串之间的连接也可以不使用加号std::string vertexShader ="#version 330 core\n""\n""layout(location = 0) in vec4 position;"  //layout(location) 限定符用于确保顶点数据与着色器中的变量正确对应,从而实现数据的正确传递和处理"\n""void main()\n""{\n""   gl_Position = position;\n""}\n";std::string fragmentShader ="#version 330 core\n""\n""layout(location = 0) out vec4 color;"  "\n""void main()\n""{\n""   color = vec4(1.0,0.0,0.0,1.0);\n""}\n";unsigned int shader = CreateShader(vertexShader, fragmentShader);//绑定着色器glUseProgram(shader);

8. 自己的总结

  1. 说OpenGL是一种图形API,是因为它提供了一组函数和规范用于描述和操作图形硬件。然而它本身并不是API,而是一种规范,这个规范定义了函数和操作的方式,以及它们应该如何工作。

  2. 使用GLEW的注意事项:

(1)首先需要创建一个有效的渲染OpenGL上下文,然后调用glewInit()去初始化扩展的入口。所以不能直接从GLEW中直接调用OpenGL函数,直到你调用了glewInit()。

(2)在调用glewInit()之前,需要先创建一个渲染OpenGL的上下文。

(3)当需要用到glew.h头文件时,要在包含任何其他OpenGL相关的头文件之间就要先包含glew.h。也就是glew.h要放在前面。

  1. OpenGL管道的工作原理是我们为我们的显卡提供数据,我们在显卡上存储一些内存,它包含了我们想要绘制的所有数据,然后我们使用一个着色器,利用在显卡上执行的程序来读取数据并且完全显示在屏幕上。实际上,我们不得不告诉OpenGL,内存中有什么,又是如何布局的。如果我们不这样做,OpenGL看到的只是一堆字节。glVertexAttribPointer可以为我们做这件事,只有让OpenGL知道了我们的内存布局,它才知道怎么去正确地解析它。

  2. 着色器:

    着色器是运行在GPU上的程序,可编程的有顶点着色器和片段着色器。顶点着色器是有多少个顶点就运行多少次,而片段着色器是有多少个像素就运行多少次。

    顶点着色器(确定顶点的最终位置)中,一般以顶点位置、顶点法线、颜色、纹理坐标作为输入,具体的视具体情况而定。而一般以gl_Position作为输出,用于表示变换后的顶点位置。

    片段着色器中(确定每个像素的颜色),一般以顶点属性(如位置、法线、纹理坐标等)和uniform变量作为输入。一般以像素颜色为输出。

  3. 关于有一个性能优化的问题:

    就是例如还是在这个三角形中,因为顶点只有三个,所以只会调用顶点着色器三次;而像素点的数量会取决于三角形的大小,如果三角形比较大,则像素点会比较多,而每个像素都会调用一次片段着色器。所以有时可以考虑一些事情在顶点着色器处理就好,不用去到片段着色器。片段着色器里的东西代价要高得多,因为片段着色器会为每个像素运行。

  4. 完整的图形渲染管线流程:

    (1)准备阶段:初始化glfw,设置窗口属性(glfwWindowHint())、创建窗口(glfwCreateWindow())、设置OpenGL渲染上下文、初始化glew和渲染之前告诉OpenGL渲染窗口的尺寸大小(glfwGetFramebuffersize()和glViewPort()),把顶点数据传递给顶点着色器;

    (2)顶点着色器:把一个单独的顶点作为输入,确定每个顶点的位置。根据要求对每个顶点进行变换,例如平移、旋转、缩放等;

    (3)图元装配:将顶点着色器输出的所有顶点作为输入,并将所有顶点装配成指定的图元形状。

    (4)几何着色器:把图元形式的一系列顶点的集合作为输入,对图元进行下一步的变换和处理,例如生成新的顶点、改变图元形状等。

    (5)光栅化:将图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段。

    (6)片段着色器:计算每个像素的最终颜色,包括纹理、材质、光照等。

    (7)Alpha测试和混合:这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。Alpha测试是指当不同物体重叠且在屏幕的不同深度时,会根据物体的深度和透明度对物体的不同显示或忽略。当两个物体一前一后时,前面物体又是透明的话,此时就会对它们进行混合。

  5. 纹理:

    为了能够把纹理映射到图形上,我们需要指定图形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标,用来标明该从纹理图像的哪个部分采样(采集片段颜色)。之后在图像的其他片段上进行片段插值。纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样。它可以采用几种不同的插值方式,所以我们需要自己告诉OpenGL该怎样对纹理采样。

    对于纹理,一定要指定这四个参数,过滤器(放大、缩小两个)和环绕方式(GL_TEXTURE_WRAP_S水平环绕、GL_TEXTURE_WRAP_T垂直环绕,有点像x和y轴),如果不指定,就会得到一个黑色的纹理,因为OpenGL有给它们默认值,默认情况下就是黑色。

  6. 在坐标变换中,通常会设定一个坐标范围,再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器,将它们变换为屏幕上的二维坐标或像素。将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的。过程通常是这样:

    局部坐标(就是我们自己定义的坐标) —根据模型矩阵(由平移、旋转和缩放操作组成)—> 世界坐标 —根据观察矩阵(用于定义虚拟摄像机的位置和朝向,将场景从全局坐标系转换到摄像机的局部坐标系) —> 观察坐标 —根据投影矩阵(用于将三维场景中的物体坐标转换为二维屏幕坐标,实现透视效果和深度感)—> 裁剪坐标(经过裁剪和透视除法等操作,将坐标变换为标准化设备坐标的范围(-1.0,1.0)) —视口变换和垂直翻转—> 屏幕坐标。

    以上状态是一个顶点在最终被转化为片段之前需要经历的。

  7. 摄像机:

    当讨论摄像机/观察空间时,是在讨论一摄像机的视角作为场景远点所有顶点的坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,需要确定它在世界空间中的位置、观察的方向、一个指向它右侧的向量以及一个指向它上方的向量。摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量。

  8. 颜色:

    当在OpenGL中创建一个光源时,希望给光源一个颜色,利用颜色反射的定律,我们先将光源设置为白色。然后用光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色)。这里说的颜色都是一个向量。

  9. 光照:

    在OpenGL中,当要使用光照时可以使用冯氏光照模型。该模型的主要结构由3个分量组成:环境、漫反射和镜面光照。环境光照分量会永远给物体一些颜色,不至于使物体完全黑暗;漫反射光照是为了模拟光源对物体方向性的影响,如物体的某部分越是正对着光源,它就越亮;镜面光照是为了模拟有光泽物体上面出现的亮点,它的颜色相比于物体颜色会更倾向于光的颜色。

  10. 材质:
    当描述一个表面时,可以分别为三个光照分量(环境、漫反射和镜面)定义一个材质颜色。通过为每个分量指定一个颜色,我们就能够对表面的颜色输出有细粒度的控制。再添加一个反光度分量,结合上上述的三个颜色,就有了全部所需的材质属性了。ambient(环境)材质向量定义了在环境光照下这个表面反射的是什么颜色,通常与表面的颜色相同。diffuse(漫反射)材质向量定义了在漫反射光照下表面的颜色。漫反射颜色(和环境光照一样)也被设置为我们期望的物体颜色。specular(镜面)材质向量设置的是表面上镜面高光的颜色(或者甚至可能反映一个特定表面的颜色)。最后,shininess影响镜面高光的散射/半径。
    材质是用于描述物体的外观和反射特性的属性集合。它定义了物体的颜色、光照响应以及其他与表面特性相关的属性。

相关文章:

The Cherno——OpenGL

The Cherno——OpenGL 1. 欢迎来到OpenGL OpenGL是一种跨平台的图形接口&#xff08;API&#xff09;&#xff0c;就是一大堆我们能够调用的函数去做一些与图像相关的事情。特殊的是&#xff0c;OpenGL允许我们访问GPU&#xff08;Graphics Processing Unit 图像处理单元&…...

linux中学习控制进程的要点

1. 进程创建 1.1 fork函数 #include <unistd.h> pid_t fork(void); 返回值&#xff1a;自进程中返回0&#xff0c;父进程返回子进程id&#xff0c;出错返回-1 进程调用fork&#xff0c;当控制转移到内核中的fork代码后&#xff0c;内核会做以下操作 分配新的内存块和…...

C++Qt QSS要注意的坑

qss源自css&#xff0c;相当于css的一个子集&#xff0c;主要支持的是css2标准&#xff0c;很多网上的css3的标准的写法在qss这里是不生效的&#xff0c;所以不要大惊小怪。 qss也不是完全支持所有的css2&#xff0c;比如text-align官方文档就有说明&#xff0c;只支持 QPushB…...

LeetCode每日一题:56. 合并区间(2023.8.27 C++)

目录 56. 合并区间 题目描述&#xff1a; 实现代码与解析&#xff1a; 排序 贪心 原理思路&#xff1a; 56. 合并区间 题目描述&#xff1a; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&…...

电视盒子什么牌子好?经销商整理线下热销电视盒子品牌排行榜

在面对众多品牌和型号时&#xff0c;不知道电视盒子哪个牌子好的消费者超多&#xff0c;很多人进店都会问我电视盒子哪款好&#xff1f;我根据店铺内近两年的销量情况整理了电视盒子品牌排行榜&#xff0c;看看实体店哪些电视盒子最值得入手吧。 TOP 1.泰捷WEBOX 40Pro Max电视…...

JavaScript关于函数的小挑战

题目 回到两个体操队&#xff0c;即海豚队和考拉队! 有一个新的体操项目&#xff0c;它的工作方式不同。 每队比赛3次&#xff0c;然后计算3次得分的平均值&#xff08;所以每队有一个平均分&#xff09;。 只有当一个团队的平均分至少是另一个团队的两倍时才会获胜。否则&…...

机器学习深度学习——针对序列级和词元级应用微调BERT

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——NLP实战&#xff08;自然语言推断——注意力机制实现&#xff09; &#x1f4da;订阅专栏&#xff1a;机…...

重启Mysql时报错rm: cannot remove ‘/var/lock/subsys/mysql‘: Permission denied

只有用mysql重启时报错&#xff0c;用root不报错 [mysqlt3-dtpoc-dtpoc-web04 bin]$ service mysql restart Shutting down MySQL.. SUCCESS! rm: cannot remove /var/lock/subsys/mysql: Permission denied Starting MySQL.. SUCCESS! [roott3-dtpoc-dtpoc-web04 ~]# serv…...

[C/C++]指针详讲-让你不在害怕指针

个人主页&#xff1a;北海 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;C/C&#x1f91d;希望作者的文章能对你有所帮助&#xff0c;有不足的地方请在评论区留言指正&#xff0c;大家一起学习交流&#xff01;&#x1f9…...

无涯教程-Android - Frame Layout函数

Frame Layout 旨在遮挡屏幕上的某个区域以显示单个项目&#xff0c;通常&#xff0c;应使用FrameLayout来保存单个子视图&#xff0c;因为在子视图彼此不重叠的情况下&#xff0c;难以以可扩展到不同屏幕尺寸的方式组织子视图。 不过&#xff0c;您可以使用android:layout_grav…...

docker desktop安装es 并连接elasticsearch-head:5

首先要保证docker安装成功&#xff0c;打开cmd&#xff0c;输入docker -v&#xff0c;出现如下界面说明安装成功了 下面开始安装es 第一步&#xff1a;拉取es镜像 docker pull elasticsearch:7.6.2第二步&#xff1a;运行容器 docker run -d --namees7 --restartalways -p 9…...

计网(第四章)(网络层)(六)

目录 一、路由选择协议&#xff08;动态路由自动获取路由信息&#xff09;概述&#xff1a; 二、因特网采用的路由协议 主要特点&#xff1a; 1.自适应 2.分布式 3.分层次 因特网采用分层次的路由选择协议&#xff1a; 三、常见的路由选择协议 一、路由选择协议&#xff…...

科研无人机平台P600进阶版,突破科研难题!

随着无人机技术日益成熟&#xff0c;无人机的应用领域不断扩大&#xff0c;对无人机研发的需求也在不断增加。然而&#xff0c;许多开发人员面临着无法从零开始构建无人机的时间和精力压力&#xff0c;同时也缺乏适合的软件平台来支持他们的开发工作。为了解决这个问题&#xf…...

Apache的简单介绍(LAMP架构+搭建Discuz论坛)

文章目录 1.Apache概述1.1什么是apache1.2 apache的功能及特性1.2.1功能1.2.2特性 1.3 MPM 工作模式1.3.1 prefork模式1.3.2 worker模式1.3.3 event模式 2.LAMP概述2.1 LAMP的组成2.2 LAMP各组件的主要作用2.3 LAMP的工作过程2.4CGI和FastCGI 3.搭建Discuz论坛所需4.编译安装Ap…...

CDL基础原理

一、CDL简介 CDL&#xff08;全称Change Data Loader&#xff09;是一个基于Kafka Connect框架的实时数据集成服务。 CDL服务能够从各种OLTP数据库中捕获数据库的Data Change事件&#xff0c;并推送到kafka&#xff0c;再由sink connector推送到大数据生态系统中。 CDL目前支…...

WPF基础入门-Class7-WPF-MVVN框架

WPF基础入门 Class7-MVVN框架 使用框架可以省掉如Class6中的ViewModelBase.cs的OnPropertyChanged&#xff0c;亦方便命令传参 1、NuGet安装CommunityToolkit.Mvvm&#xff08;原Mircrosoft.Toolkit.Mvvm&#xff09;也可以安装MVVMLight等其他集成库 2、显示页面&#xff1…...

C语言练习题第三弹!!!绝对典中典!!!

目录 1.单身狗1 1.1 题目 1.2 分析推理 1.3 代码实现 2.单身狗2 2.1 题目 2.2 分析推理 2.3 代码实现 3.字符串左旋 3.1 题目 3.2 分析推理 3.3 代码实现 3.3.1 方法一 3.3.2 优化一 3.3.2.1 思路分析 3.3.2.2 strcpy函数和strncat函数 3.3.2.3 代码实现 3.3.…...

Jedis

Jedis 使用Java操作redis Jedis是redis官方推荐的Java连接开发工具&#xff01; 使用Java操作redis的中间件 测试 导入对应的依赖 <!-- https://mvnrepository.com/artifact/redis.clients/jedis --><dependency><groupId>redis.clients</groupId&g…...

Linux 使用TCP_INFO查询TCP连接的状态信息

Linux 上可以使用TCP_INFO查询TCP连接状态信息包括&#xff1a; 发送方拥塞窗口阈值、发送方缓冲区拥塞窗口、advmss&#xff08;Advertised MSS&#xff09;、通过 ACK 确认的累计字节数等等 struct tcp_info {__u8 tcpi_state;__u8 tcpi_ca_state;__u8 tcpi_retransmits;__…...

软件测试案例 | 气象探测库存管理系统的集成测试计划

将经过单元测试的模块按照设计要求连接起来&#xff0c;组成规定的软件系统的过程被称为“集成”。集成测试也被称为组装测试、联合测试、子系统测试或部件测试等&#xff0c;其主要用于检查各个软件单元之间的接口是否正确。集成测试同时也是单元测试的逻辑扩展&#xff0c;即…...

vue点击按钮重新加载页面(vue第一次加载页面点击按钮出现页面刷新问题之后一切正常)

问题描述 所开发的vue项目每次跑起来之后就会出现点击按钮后重新加载整个页面的问题&#xff0c;但是只会在第一次点击的时候出现&#xff0c;后面就不会在出现加载整个页面的情况。 原因 在form表单中使用button按钮导致form表单进行了页面刷新。button默认的“type‘submi…...

软件工程(十一) 系统设计分类

我们知道需求规格说明书(SRS)落地之后, 就要开始着手系统设计了,看一下这个系统该如何来设计,并且如何实现。学习系统设计之前,需要先了解系统设计有哪些分类。 系统设计的分类如下 界面设计结构化设计面向对象设计(最重要)1、界面设计 界面设计也叫做人机界面设计,属于…...

数字转中文大写金额

有时候&#xff0c;我们需要显示中文大写金额&#xff0c;比如打印银行付款申请单等。 新建一个工程&#xff0c;加入一个标准模块在模块中加入如下代码&#xff0c;窗口中调用 AmountInChineseWords 函数即可。最大解析到百万亿&#xff0c;小数最多解析两位到分。 模块代码…...

Java——HashMap和HashTable的区别

Java——HashMap和HashTable的区别 Java HashMap和HashTable的区别1. 继承的父类2. 线程安全性3. null值问题4. 初始容量及扩容方式5. 遍历方式6. 计算hash值方式 Java HashMap和HashTable的区别 1. 继承的父类 都实现了Map、Cloneable&#xff08;可复制&#xff09;、Seria…...

Docker去除sudo权限

Docker去除sudo权限 使用docker命令时&#xff0c;每次都要sudo提权&#xff0c;否则就会报错提示无权限。 1.查看docker用户组及成员 sudo cat /etc/group | grep docker2.添加docker用户组 sudo groupadd docker3.添加用户到docker组 sudo gpasswd -a ${USER} docker4.增…...

【ROS系统】Ubuntu22.04系统中安装ROS2系统_ubuntu 安装ros2_GoesM

【ROS系统】Ubuntu22.04系统中安装ROS2系统_ubuntu 安装ros2_GoesM Excerpt ROS仿真、专为自动驾驶研发提供的系统平台_ubuntu 安装ros2 参考博客&#xff1a;ROS 安装详细教程 —— Ubuntu22.0.4 LTS 安装 Part 0. 准备 首先&#xff0c;我们需要一个Ubuntu系统。 Part 1. …...

MySQL8.0.22安装过程记录(个人笔记)

1.点击下载MySQL 2.解压到本地磁盘&#xff08;注意路径中不要有中文&#xff09; 3.在解压目录创建my.ini文件 文件内容为 [mysql] # 设置mysql客户端默认字符集 default-character-setutf8[mysqld] # 设置端口 port 3306 # 设计mysql的安装路径 basedirE:\01.app\05.Tool…...

Python中pip和conda的爱恨情仇

在使用pip和conda时&#xff0c;是否也有过以下的疑惑&#xff1f;&#xff1f;&#xff1f; 目前只总结了以下常见的几种混淆&#xff0c;如有学者还有其它疑惑&#xff0c;欢迎留言讨论&#xff0c;我会解答更新&#xff0c;帮助自己理清的同时&#xff0c;也帮助其他同样困…...

HTTPS协议原理

目录 前言 1.理解加密和解密 2.为什么要加密 3.常见的加密方式 3.1对称加密 3.2非对称加密 4.数据摘要和数据指纹 5. 数字签名 6.HTTPS的加密策略 6.1只使用对称加密 6.2使用非对称加密 6.2.1服务端使用非对称加密 6.2.2双方都使用非对称加密 6.3对称加密非对称加…...

C语言每日一练------Day(6)

本专栏为c语言练习专栏&#xff0c;适合刚刚学完c语言的初学者。本专栏每天会不定时更新&#xff0c;通过每天练习&#xff0c;进一步对c语言的重难点知识进行更深入的学习。 今日练习题关键字&#xff1a;整数转换 异或 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn…...

springboot中使用ElasticSearch

引入依赖 修改我们的pom.xml&#xff0c;加入spring-boot-starter-data-elasticsearch <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>编写配…...

十二、集合(2)

本章概要 添加元素组集合的打印列表 List 添加元素组 在 java.util 包中的 Arrays 和 Collections 类中都有很多实用的方法&#xff0c;可以在一个 Collection 中添加一组元素。 Arrays.asList() 方法接受一个数组或是逗号分隔的元素列表&#xff08;使用可变参数&#xff…...

【网络设备】交换机的概念、工作原理、功能以及以太网帧格式

个人主页&#xff1a;insist--个人主页​​​​​​ 本文专栏&#xff1a;网络基础——带你走进网络世界 本专栏会持续更新网络基础知识&#xff0c;希望大家多多支持&#xff0c;让我们一起探索这个神奇而广阔的网络世界。 目录 一、认识交换机 二、交换机的主要功能 1、数…...

研磨设计模式day11观察者模式

目录 场景 代码示例 定义 观察者模式的优缺点 本质 何时选用 简单变型-区别对待观察者 场景 我是一家报社&#xff0c;每当我发布一个新的报纸时&#xff0c;所有订阅我家报社的读者都可以接收到 代码示例 报纸对象 package day11观察者模式;import java.util.Observ…...

第八周第二天学习总结 | MySQL入门及练习学习第四天

实操练习&#xff1a; 1.建立一个员工表和与之对应的部门表 2.建立外键约束 3.使用多表查询&#xff0c;直接查询部门表和员工表 发现&#xff1a;有很多多余的因笛卡尔乘积而带来的多余输出内容 我想要的到简单明了的数据结果&#xff0c;要消除多于因笛卡尔乘积带来的输出…...

WPF数据转换

在基本绑定中&#xff0c;信息从源到目标的传递过程中没有任何变化。这看起来是符合逻辑的&#xff0c;但我们并不总是希望出现这种行为。通常&#xff0c;数据源使用的是低级表达方式&#xff0c;我们可能不希望直接在用户界面使用这种低级表达方式。WPF提供了两个工具&#x…...

《Go 语言第一课》课程学习笔记(十三)

方法 认识 Go 方法 Go 语言从设计伊始&#xff0c;就不支持经典的面向对象语法元素&#xff0c;比如类、对象、继承&#xff0c;等等&#xff0c;但 Go 语言仍保留了名为“方法&#xff08;method&#xff09;”的语法元素。当然&#xff0c;Go 语言中的方法和面向对象中的方…...

基于RUM高效治理网站用户体验入门-价值篇

用户体验 用户体验基本包含访问网站的性能、可用性和正确性。通俗的讲&#xff0c;就是一把通过用户访问测量【设计者】意图的尺子。 本文目的 网站如何传递出设计者的意图&#xff0c;可能页面加载时间太长、或者页面在用户的浏览器中渲染时间太慢&#xff0c;或者第三方设备…...

Unity之Photon PUN2开发多人游戏如何实现组队功能

前言 Photon Unity Networking 2 (PUN2) 是一款基于Photon Cloud的Unity多人游戏开发框架。它提供了一系列易于使用的API和工具,使开发者可以快速构建多人戏,并轻松处理多人游戏中的网络同步、房间管理、玩家匹配等问题。 我们在查看Pun2的Demo时,会发现Demo中自带了一个简…...

大数据Flink简介与架构剖析并搭建基础运行环境

文章目录 前言Flink 简介Flink 集群剖析Flink应用场景Flink基础运行环境搭建Docker安装docker-compose文件编写创建并运行容器访问Flink web界面 前言 前面我们分别介绍了大数据计算框架Hadoop与Spark,虽然他们有的有着良好的分布式文件系统和分布式计算引擎&#xff0c;有的有…...

RISC-V IOPMP实际用例-Rapid-k模型在NVIDIA上的应用

安全之安全(security)博客目录导读 2023 RISC-V中国峰会 安全相关议题汇总 说明&#xff1a;本文参考RISC-V 2023中国峰会如下议题&#xff0c;版权归原作者所有。...

【UE5】给模型指定面添加自定义材质

实现步骤 1. 首先我们向UE中导入一个简单的模型&#xff0c;可以看到目前该模型的材质插槽只有一个&#xff0c;当我们修改材质时会使得模型整体的材质全部改变&#xff0c;如果我们只想改变模型的某些面的材质就需要继续做后续操作。 2. 选择建模模式 3. 在模式工具栏中点击…...

mall:redis项目源码解析

文章目录 一、mall开源项目1.1 来源1.2 项目转移1.3 项目克隆 二、Redis 非关系型数据库2.1 Redis简介2.2 分布式后端项目的使用流程2.3 分布式后端项目的使用场景2.4 常见的缓存问题 三、源码解析3.1 集成与配置3.1.1 导入依赖3.1.2 添加配置3.1.3 全局跨域配置 3.2 Redis测试…...

RISC-V Linux系统kernel制作

文章目录 1、下载2、编译 1、下载 Linux 官网地址:https://www.kernel.org $ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.181.tar.xz $ tar xvf linux-5.10.181.tar.xz $ cd linux-5.10.1812、编译 安装依赖 $ sudo apt-get install -y flex bison bui…...

5G NR:PRACH时域资源

PRACH occasion时域位置由高层参数RACH-ConfigGeneric->prach-ConfigurationIndex指示&#xff0c;根据小区不同的频域和模式&#xff0c;38.211的第6.3.3节中给出了prach-ConfigurationIndex所对应的表格。 小区频段为FR1&#xff0c;FDD模式(paired频谱)/SUL&#xff0c;…...

LLaMA-2的模型架构

输入token&#xff1b;[B, L] 输出probs:[B, L, vab_size]...

掌握Java框架之Struts,开启高效开发之旅!

当今的软件开发世界&#xff0c;Java框架如Struts已经成为构建企业级应用的重要工具。Struts作为一个流行的MVC框架&#xff0c;不仅简化了Java Web开发&#xff0c;还提高了软件的可维护性和可扩展性。本文将带你走进Struts的世界&#xff0c;探索其魅力所在&#xff0c;让你领…...

关于Vue.set()

简介 Vue.set() 是 Vue 中的一个全局方法&#xff0c;其主要作用是向响应式对象添加新的属性&#xff0c;并确保新属性同样具有响应式。在 Vue.js 中&#xff0c;当数据对象的属性被直接修改时&#xff0c;Vue 可以监测到数据变化并响应变化。但若添加新的响应式对象属性时&am…...

Selenium 遇见伪元素该如何处理?

问题发生 在很多前端页面中&#xff0c;大家会见到很多&#xff1a;:before、::after 元素&#xff0c;比如【百度流量研究院】&#xff1a; 比如【百度疫情大数据平台】&#xff1a; 以【百度疫情大数据平台】为例&#xff0c;“累计确诊”文本并没有显示在 HTML 源代码中&am…...

RPA技术介绍与应用价值

一、什么是RPA技术? RPA(Robotic Process Automation)机器人流程自动化,是一种能够模拟人类来执行重复性任务的新型技术。RPA可实现统筹安排、自动化业务处理,并提升业务工作流处理效率。用户只需通过图形方式显示的计算机操作界面对RPA软件进行动态设定即可。借助RPA (R…...