Learn OpenGL


title: Learn OpenGL 入门
date: 2024-05-05 22:00:00
categories:

  • OpenGL
  • 入门
    tags: 创建图形到摄像机总结

OpenGL

学习也有两个多礼拜,结束了 LearnOpenGL CN 的入门部分的学习,是时候总结整理一下了,按照整体创建一个图形的代码步骤,大体上也是渲染管线的流程。全部按照逻辑顺序来写,方便之后忘了回来可以快速记起这个顺序

详见猴子也能看懂的渲染管线(Render Pipeline) - 知乎 (zhihu.com)

opengl是什么

OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)。

早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来,开发者很少有控制OpenGL如何进行计算的自由。而开发者迫切希望能有更多的灵活性。随着时间推移,规范越来越灵活,开发者对绘图细节有了更多的掌控。立即渲染模式确实容易使用和理解,但是效率太低。因此从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。

状态机

OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。

假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。

当使用OpenGL的时候,我们会遇到一些状态设置函数(State-changing Function),这类函数将会改变上下文。以及状态使用函数(State-using Function),这类函数会根据当前OpenGL的状态执行一些操作。只要你记住OpenGL本质上是个大状态机,就能更容易理解它的大部分特性。

世界就是一个巨大的状态机,State Machine!!!!!

绘制

有关配置及窗口部分不在赘述,重在理解各个对象

  • 顶点数组对象:Vertex Array Object,VAO
  • 顶点缓冲对象:Vertex Buffer Object,VBO
  • 元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO

使用OpenGL绘制图形的流程大致可以分为以下几个步骤:

  1. 创建OpenGL上下文(Context):在调用任何OpenGL指令之前,首先需要创建一个OpenGL上下文。这个上下文是一个庞大的状态机,保存了OpenGL中的各种状态,是OpenGL指令执行的基础。
  2. 设置帧缓冲区(FrameBuffer):帧缓冲区相当于画板,它不是实际存储数据的对象,而是提供了附着点(Attachment),可以附着纹理(Texture)或渲染缓冲区(RenderBuffer)。
  3. 准备顶点数据和缓冲区:顶点数据是图形的骨架,可以存储在顶点数组(VertexArray)中,也可以预先传入显存中的顶点缓冲区(VertexBuffer)。
  4. 设置索引数据和缓冲区:索引数据(ElementArray)可以实现顶点的复用,提高效率。它们可以存储在索引缓冲区(ElementBuffer)中。
  5. 编写和编译着色器程序(Shader):现代OpenGL使用可编程渲染管线,需要编写顶点着色器(VertexShader)和片段着色器(FragmentShader),然后将它们编译成着色器程序。
  6. 设置渲染管线和绘制命令:配置好渲染管线后,使用绘制命令如glDrawArraysglDrawElements来执行绘制操作。
  7. 渲染到屏幕:经过光栅化等一系列处理后,图形最终会渲染到屏幕上。

初始化

从代码开始

  /* Initialize the library */
 	//初始化GLFW
    if (!glfwInit()) 
        return -1;
	//指定版本
    const char* glsl_version = "#version 330 core";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号(Major)
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号(Minor)
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//核心模式(Core-profile)

    /* 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);
	//启用垂直同步
    glfwSwapInterval(3);

	//错误检查
    if(glewInit()!=GLEW_OK) 
        std::cout<<"Error \n";
	
    std::cout << glGetString(GL_VERSION) << std::endl;//输出当前显卡配置版本

这部分在最开始,初始化glfw,指定版本(核心模式) ,创建窗口,创建上下文,启用垂直同步。

开始!

开始绘制图形之前,我们需要先给OpenGL输入一些顶点数据,OpenGL仅当3D坐标在3个轴(x、y和z)上-1.0到1.0的范围内时才处理,所有在这个范围内的坐标叫做标准化设备坐标,

希望渲染一个三角形,就要指定三个顶点,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。

float vertices[] = {
		//位置			     //颜色            //纹理    
		0.5f,  0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  1.0f,1.0f,     // 右上角
		0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  1.0f,0.0f,     // 右下角
	   -0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f,  0.0f,0.0f,     // 左下角
	   -0.5f,  0.5f, 0.0f,  0.0f, 0.5f, 0.0f,  0.0f,1.0f     // 左上角
    };
unsigned int index[] = {
		// 注意索引从0开始!
		// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
		// 这样可以由下标代表顶点组合成矩形
		0, 1, 3,  // 第一个三角形
		1, 2, 3,  // 第二个三角形

	};

这里的两个数组分别对应的VB和IB

vertices存放所有的顶点,每一行代表一个顶点,每个顶点有三个顶点属性,分别为位置颜色纹理

位置属性负责指定顶点位置,颜色属性指定顶点颜色,纹理属性指定纹理与顶点的映射

index存放索引,用于指定或是说告诉GPU如何使用顶点,这里表示使用第0,1,3 和第1,2,3这几个顶点绘制两个三角形,然后指定出了一个矩形

VAO

先指定一个VAO,**Vertex Array Object(顶点数组对象) ** ,用于管理和优化顶点数据的存储和访问,可以说VAO绑定期间内的其他所有的VBO和IBO的状态都会被VAO保存(状态机)

	unsigned int VAO;

	glGenVertexArrays(1, &VAO);//生成1个VA并返回id给VAO
	glBindVertexArray(VAO);//绑定VAO

VBO

绑定可以理解为插到一个对应的插槽上,例如这里就是将VBO绑定在GL_ARRAY_BUFFER这个插槽上,之后任何对GL_ARRAY_BUFFER的操作就相当于是对这个VBO的操作,同时也意味着,如果要对另一个VBO操作就需要将另一个VBO绑定在这个插槽上,(IBO同理)

顶点缓冲对象类型:GL_ARRAY_BUFFER

unsigned int VBO;
	glGenBuffers(1, &VBO);				//生成1个buffer并返回id给VBO
	glBindBuffer(GL_ARRAY_BUFFER, VBO);	  //绑定VBO到GL_ARRAY_BUFFER上
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), &vertices, GL_STATIC_DRAW);//把用户定义的数据复制到当前绑定缓冲的函数
//有一些顶点数据存储在 position 中,把这些数据的大小为 6 * sizeof(float) 的部分传输到GPU(buffer)。多次使用这些数据来绘制,但不会对它们进行修改

IBO

此处的GL_ELEMENT_ARRAY_BUFFER就是另一个插槽,

索引缓冲对象类型:GL_ELEMENT_ARRAY_BUFFER

unsigned int IBO;
	glGenBuffers(1, &IBO);		//生成1个buffer并返回id给IBO
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);//绑定VBO到GL_ELEMENT_ARRAY_BUFFER上
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(index), &index, GL_STATIC_DRAW);//将索引数据复制到缓冲中-告诉GPU使用这几个索引来绘制

Texture

VBO和IBO已经告诉了GPU如何绘制这个图形,接下来给这个图形添加纹理,在开始部分的顶点属性中的位置,颜色,纹理 我们已经完成了前两个的设置,

我们已经知道可以为每个顶点添加颜色来增加图形的细节,从而创建出有趣的图像。但是,如果想让图形看起来更真实,我们就必须有足够多的顶点,从而指定足够多的颜色。这将会产生很多额外开销,因为每个模型都会需求更多的顶点,每个顶点又需求一个颜色属性。

艺术家和程序员更喜欢使用纹理(Texture)。纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。

准备工作
	stbi_set_flip_vertically_on_load(true);//上下翻转,因为纹理种坐标左下角为(0,0),可能会上下颠倒

	unsigned char* data0 = stbi_load("res/texture/bbb.png", &width0, &height0, &nrChannels0, 4);
	unsigned char* data1 = stbi_load("res/texture/aaa.png", &width1, &height1, &nrChannels1, 4);

	//首先接受一个图像文件的位置作为输入,接下来它需要三个int用,图像的宽度、高度和颜色通道的个数填充这三个变量
	
纹理绑定
	unsigned int texture0;//与vbo,vao,ibo类似,都是使用id引用
	glGenTextures(1, &texture0);//生成纹理id并传入texture
	glBindTexture(GL_TEXTURE_2D, texture0);//绑定纹理
纹理设置

设置当前纹理的环绕&过滤

//设置当前纹理的环绕
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);//为当前绑定在GL_TEXTURE_2D上的纹理的s轴进行GL_REPEAT的环绕方式
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);//为当前绑定在GL_TEXTURE_2D上的纹理的t轴进行GL_REPEAT的环绕方式
	
//设置当前纹理的过滤
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//为当前绑定在GL_TEXTURE_2D上的纹理的放大行为设置GL_LINEAR_MIPMAP_LINEAR的多级渐远纹理方式
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	
	/*一个常见的错误是,将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何效果,因为多级渐远纹理
	  主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产
	  生一个GL_INVALID_ENUM错误代码。*/

现在已经有了准备好的图片数据data和设置好的纹理texture,我们可以使用前面载入的图片数据生成一个纹理了。纹理可以通过glTexImage2D来生成:

	if (data0)
	{
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width0, height0, 0, GL_RGBA, GL_UNSIGNED_BYTE, data0);//使用载入的图片数据生成纹理
		
        //target,多级渐远纹理级别,纹理存储格式,最终的纹理的宽度和高度,0,原图的格式及数据类型,图像数据(stbi_load获取)
		//目前只有基本级别(Base-level)的纹理图像被加载了,要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)
		//或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
		glGenerateMipmap(GL_TEXTURE_2D);//这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
		
	}

生成纹理的过程总体是这样的:

unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);   
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
    std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);//释放图像的内存

着色器代码

目前阶段的渲染管线中只需要关心顶点着色器和片段着色器,着色器语言GLSL。

着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。

一个示例结构:

#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

int main()
{
  // 处理输入并进行一些图形操作
  ...
  // 输出处理过的结果到输出变量
  out_variable_name = weird_stuff_we_processed;
}

GLSL定义了inout关键字专门来实现输入和输出,进行数据交流和传递。

顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据(VB)中直接接收输入,为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。我们已经在前面的教程看过这个了,layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。

如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)

//顶点着色器
	const char* vertexshadersource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0 
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
layout (location = 2) in vec2 aTexCoord; //纹理变量的属性位置

out vec3 ourColor; // 向片段着色器输出一个颜色
out vec2 TexCoord; // 向片段着色器输出一个纹理

//uniform mat4 transform;
uniform mat4 model;
uniform mat4 view;
uniform mat4 proj;

uniform mat4 u_MVP;

void main()
{
    gl_Position = u_MVP * vec4(aPos, 1.0f);
    ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
	TexCoord = aTexCoord;
}
)";

//片段着色器
	const char* fragmentshadersource = R"(
#version 330 core
out vec4 FragColor;  

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture0;
uniform sampler2D ourTexture1;

void main()
{
    //FragColor = vec4(ourColor, 1.0);//为了片段插值提供数据
	//FragColor = texture(ourTexture, TexCoord);//GLSL内建的texture函数来采样纹理的颜色
	//FragColor = texture(ourTexture0, TexCoord)*vec4(ourColor,1.0);//把纹理颜色与顶点颜色在片段着色器中相乘来混合二者的颜色
	FragColor = mix(texture(ourTexture0,TexCoord),texture(ourTexture1,TexCoord),0.4);//mix函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值,0.2会返回80%的第一个输入颜色和20%的第二个输入颜色,即返回两个纹理的混合色
}
)";
	

有了着色器代码就可以开始定义着色器

	// 创建一个空的着色器对象,并返回一个可以引用它的非零值	
	unsigned int vertexShader; 
	vertexShader = glCreateShader(GL_VERTEX_SHADER);	

	glShaderSource(vertexShader, 1, &vertexshadersource, NULL);//把这个着色器源码附加到着色器对象,要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量
	glCompileShader(vertexShader);// 编译着色器

	unsigned int fragmentShader;
	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShader, 1, &fragmentshadersource, NULL);
	glCompileShader(fragmentShader);

着色器程序

创建一个程序对象很简单:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();//创建一个程序并返回id

把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们:

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:

glUseProgram(shaderProgram);

着色器对象链接到程序对象以后,删除着色器对象,不再需要它们:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

现在,我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。就快要完成了,但还没结束,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。我们需要告诉OpenGL怎么做。

应用属性

img

glVertexAttribPointer用于指定这些属性

	//设置顶点位置属性指针
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);//在这里的第一个参数使vao保存了vbo中数据应该有的状态!!!!!
	glEnableVertexAttribArray(0);
	//设置顶点颜色属性指针
	//glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
	//glEnableVertexAttribArray(1);
	//设置顶点纹理属性指针
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));

  • 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?layout就是这里的顶点属性的标号,对应position中的顶点,从前到后为位置,颜色,纹理,所以分别为0,1,2
  • 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
  • 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
  • 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
  • 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
  • 最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。

摄像机

OpenGL本身没有摄像机(Camera)的概念,但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种我们在移动的感觉,而不是场景在移动。所以摄像机就是对场景的整体模型(Model)、观察(View)、投影(Projection)矩阵进行变换来实现摄像机效果

变换矩阵

模型(Model)、观察(View)、投影(Projection)矩阵(MVP),由于矩阵运算的特性,对一个矩阵进行的运算也就是每一次变换都会被存储在这个矩阵中,所以这个矩阵就会包括所有的组合的操作。也就是将这三个矩阵存储到一个矩阵mvp中,作为一个uniform来统一操作。

注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

模型矩阵

模型矩阵(Model Matrix)将物体的坐标从局部变换到世界空间;对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。你可以将它想像为变换一个房子,你需要先将它缩小(它在局部空间中太大了),并将其位移至郊区的一个小镇,然后在y轴上往左旋转一点以搭配附近的房子。你也可以把上一节将箱子到处摆放在场景中用的那个矩阵大致看作一个模型矩阵;我们将箱子的局部坐标变换到场景/世界中的不同位置。

//创建模型矩阵,包含位移缩放旋转
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
观察矩阵

观察矩阵(View Matrix)将世界坐标变换到观察空间。通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。

//观察矩阵,将矩阵向我们要进行移动场景的反方向移动。
glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -5.0f));

投影矩阵

将顶点坐标从观察变换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix)

它指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0)。所有在范围外的坐标不会被映射到在-1.0到1.0的范围之间,所以会被裁剪掉。

将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。

//投影矩阵,在场景中使用透视投影
glm::mat4 proj = glm::mat4(1.0f);
proj = glm::perspective(glm::radians(fov), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.f);

LookAt

使用矩阵的好处之一是如果你使用3个相互垂直(或非线性)的轴定义了一个坐标空间,你可以用这3个轴外加一个平移向量来创建一个矩阵,并且你可以用这个矩阵乘以任何向量来将其变换到那个坐标空间。这正是LookAt矩阵所做的

glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), 
           glm::vec3(0.0f, 0.0f, 0.0f), 
           glm::vec3(0.0f, 1.0f, 0.0f));

glm::LookAt函数需要一个位置、目标和上向量。它会创建一个和在上一节使用的一样的观察矩阵。

在讨论用户输入之前,我们先来做些有意思的事,把我们的摄像机在场景中旋转。我们会将摄像机的注视点保持在(0, 0, 0)。

实现

有了这些矩阵,我们应该将它们传入着色器,通过uniform来设置

glm::mat4 mvp = glm::mat4(1.0f);
mvp = proj * view * model;
glUniformMatrix4fv(glGetUniformLocation(ShaderProgram, "u_MVP"), 1, GL_FALSE, glm::value_ptr(mvp));

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值