免费课程:计算机图形学OpenGL:5.2、缩放,旋转,位移_哔哩哔哩_bilibili
付费课程:计算机图形学OpenGL【合集】大礼包_腾讯课堂
我看的是免费版的
GPU渲染流程
OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。
状态机:变量(描述该如何操作)的大集合
OpenGL的状态通常被称为OpenGL上下文(Context)。
对象(Object):类似一个结构体
在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集
1.2.1 GLFW-GLAD
GLFW解决操作系统层面的不同
GLAD使代码可以可以用于不同的OpenGL驱动
注意头文件顺序,glad的include中包含了glfw所需的gl.h文件
测试代码:
#include
#include #include //用户调整窗口大小之后的回调 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } //实现输入控制 void processInput(GLFWwindow* window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//检查用户是否按下了返回键(Esc) { glfwSetWindowShouldClose(window, true); } } int main() { glfwInit(); //初始化GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号(Major) glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号(Minor) glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//核心模式(Core-profile) GLFWwindow* window = glfwCreateWindow(800, 600, "learnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window!\n" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window);//创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了 //初始化glad //glad:加载所有OpenGL的函数指针 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } //告诉OpenGL渲染窗口的大小 glViewport(0, 0, 800, 600); //设置窗口大小变化之后的回调 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //渲染循环 //在窗口结束之前,不断绘制图像,并且能够接收用户的输入 while (!glfwWindowShouldClose(window))//检查gldw是否被要求退出 { /* 双缓冲(Double Buffer) 应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的, 而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步 生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲 保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指 令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。 前缓冲区:屏幕上显示的图片 后缓冲区:正在渲染的图片 */ /* 当调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。 */ // 输入 processInput(window); //检查是否键盘输入 //渲染指令 glClearColor(0.2f, 0.3f, 0.3f, 1.0f);//设置状态 glClear(GL_COLOR_BUFFER_BIT);//使用状态 // 检查并调用事件,交换缓冲 glfwSwapBuffers(window); //交换颜色缓冲(他是一个存储glfw窗口每一个像素颜色值的大缓冲),他在这一迭代中被用来绘制,并且将作为输出显示在屏幕上 glfwPollEvents(); //检查有没有触发什么事件,并且调用对应的回调 } glfwTerminate(); return 0; }
在学习此节之前,建议将这三个单词先记下来:
- 顶点数组对象:Vertex Array Object,VAO
- 顶点缓冲对象:Vertex Buffer Object,VBO
- 元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO
图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)
图形渲染管线可以被划分为两个主要部分:
第一部分把你的3D坐标转换为2D坐标,
渲染和shader
渲染由一套渲染管线来管理
shader用来设置一些点线面的位置信息,以及映射图片到纹理这些逻辑,从而拿到图像数据,然后如何处理图像,也可以通过shader来处理。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
图形渲染管线的各个阶段的作用:
1,顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。
2,图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状;本节例子中是一个三角形。
3,图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。
4,几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据。
5,片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
6,在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
什么是VAO,VBO?
VBO用于存放数据,VAO用于描述数据的属性(如何将数据取出来)
我们的顶点缓冲数据会被解析为下面这样子:
- 位置数据被储存为32位(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
- 数据中第一个值在缓冲开始的位置。
元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。
float vertices[] = { // 第一个三角形 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, 0.5f, 0.0f, // 左上角 // 第二个三角形 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, -0.5f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f // 左上角 };
就可以转化成:
float vertices[] = { 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, -0.5f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f // 左上角 }; unsigned int indices[] = { // 注意索引从0开始! // 此例的索引(0,1,2,3)就是顶点数组vertices的下标, // 这样可以由下标代表顶点组合成矩形 0, 1, 3, // 第一个三角形 1, 2, 3 // 第二个三角形 };
着色器程序
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
函数理解:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
- 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用
layout(location = 0)
定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0
。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
。- 第二个参数指定顶点属性的大小。顶点属性是一个
vec3
,它由3个值组成,所以大小是3。- 每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性
0
现在会链接到它的顶点数据。
代码:画一个矩形
// sample_1_1_HelloWindow.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include
#include
#include
//顶点着色器
const char* vertexShaderSource ="#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
//片段着色器
const char * fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
//用户调整窗口大小之后的回调
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
//实现输入控制
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//检查用户是否按下了返回键(Esc)
{
glfwSetWindowShouldClose(window, true);
}
}
int main()
{
glfwInit(); //初始化GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号(Major)
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号(Minor)
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//核心模式(Core-profile)
GLFWwindow* window = glfwCreateWindow(800, 600, "learnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window!\n" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);//创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了
//glad:加载所有OpenGL的函数指针
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//告诉OpenGL渲染窗口的大小
glViewport(0, 0, 800, 600);
//设置窗口大小变化之后的回调
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
/************************************************************************/
/*画一个三角形 */
//顶点着色器创建
//顶点着色器附加源码,并且运行时动态编译它的源代码
unsigned int vertexShader=glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
int success = -1;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILEED\n" << infoLog << std::endl;
}
//片段着色器
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMEMT::COMPILATION_FAILEED\n" << infoLog << std::endl;
}
//着色器程序,链接着色器
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::PROGRAM::LINK\n" << infoLog << std::endl;
}
//把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
/*
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};*/
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
unsigned int indices[] = { // note that we start from 0!
0, 1, 3, // first Triangle
1, 2, 3 // second Triangle
};
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
unsigned int VBO = 0;
unsigned int VAO = 0;
unsigned int EBO = 0;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
//OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。
// OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用
// glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
//任何对GL_ARRAY_BUFFER的操作,都是对VBO的操作
//把之前定义的顶点数据复制到缓冲的内存
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
/*画一个三角形 end */
/************************************************************************/
//渲染循环
//在窗口结束之前,不断绘制图像,并且能够接收用户的输入
while (!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
//渲染指令
//激活,在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象
//(也就是之前写的着色器)了。
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glDeleteProgram(shaderProgram);
glfwTerminate();
return 0;
}
着色器语言:
默认基础数据类型:
int
、float
、double
、uint
和bool
。GLSL也有两种容器类型:分别是向量(Vector)和矩阵(Matrix)
向量:
类型
含义 vecn
包含 n
个float分量的默认向量bvecn
包含 n
个bool分量的向量ivecn
包含 n
个int分量的向量uvecn
包含 n
个unsigned int分量的向量dvecn
包含 n
个double分量的向量重组(Swizzling):
重组允许这样的语法:
vec2 someVec; vec4 differentVec = someVec.xyxx; vec3 anotherVec = differentVec.zyw; vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
vec2 vect = vec2(0.5, 0.7); vec4 result = vec4(vect, 0.0, 0.0); vec4 otherResult = vec4(result.xyz, 1.0);
一个典型的着色器有下面的结构:
#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; }
uniform:一个全局的变量
关于layout(location = attribute index) in vec3 position;语句的分析?
ayout(location=0) in vec4 vPosition_Luckie stone的博客-CSDN博客#version 430 core layout(location = 0) in vec4 vPosition; void main(){ gl_Position = vPosition;}第一行#version 430 core 表示我们所使用的4.3版本的OpenGL对应的GLSL语言,core表示使用OpenGL的核心模式。若#version没有设置,则默...https://blog.csdn.net/suyimin2010/article/details/99700711
这个文章可以解惑。
例如:layout( location=0) in vec3 position;layout( location=1) in vec3 color;
如果这个index和glVertexAttribPointer的第一个参数一样,那么相关缓存区的数据就会传递到这个变量中去。所以,
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
如果这个index=0,那么读取的相关缓存区的数据就会传递到这个变量position中去。
如果这个index=1,那么读取的相关缓存区的数据就会传递到这个变量color中去。
为什么解绑VBO之后,再解绑VAO,但是再后续使用VBO,VAO的时候,只需绑定VAO就够了?而EBO缺只能在解绑VAO之后才能解绑
因为glVertexAttribPointer函数执行之后,会影响改变VAO的状态,VBO会被复制保存到VAO中。之后如果改变了当前所绑定的缓存对象,也不会改变到VAO里的对象。
顶点着色器
int drawTriangle() { glfwInit(); //初始化GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号(Major) glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号(Minor) glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//核心模式(Core-profile) GLFWwindow* window = glfwCreateWindow(800, 600, "learnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window!\n" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window);//创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了 //glad:加载所有OpenGL的函数指针 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } //告诉OpenGL渲染窗口的大小 glViewport(0, 0, 800, 600); //设置窗口大小变化之后的回调 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); /************************************************************************/ /*画一个三角形 */ //顶点着色器创建 //顶点着色器附加源码,并且运行时动态编译它的源代码 unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource2, NULL); glCompileShader(vertexShader); int success = -1; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILEED\n" << infoLog << std::endl; } //片段着色器 unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource2, NULL); glCompileShader(fragmentShader); glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMEMT::COMPILATION_FAILEED\n" << infoLog << std::endl; } //着色器程序,链接着色器 unsigned int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if (!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "ERROR::PROGRAM::LINK\n" << infoLog << std::endl; } //把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们 glDeleteShader(vertexShader); glDeleteShader(fragmentShader); float vertices[] = { // 位置 // 颜色 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下 -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部 }; unsigned int indices[] = { // note that we start from 0! 0, 1, 3, // first Triangle 1, 2, 3 // second Triangle }; //使用glGenBuffers函数和一个缓冲ID生成一个VBO对象 unsigned int VBO = 0; unsigned int VAO = 0; unsigned int EBO = 0; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); //OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。 // OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用 // glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上 //任何对GL_ARRAY_BUFFER的操作,都是对VBO的操作 //把之前定义的顶点数据复制到缓冲的内存 glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); //当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。 // 这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否 // 则它就没有这个EBO配置了。 glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); //画成线条 //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); /*画一个三角形 end */ /************************************************************************/ //渲染循环 //在窗口结束之前,不断绘制图像,并且能够接收用户的输入 while (!glfwWindowShouldClose(window)) { // 输入 processInput(window); //渲染指令 //激活,在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象 //(也就是之前写的着色器)了。 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); //glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // 检查并调用事件,交换缓冲 glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glDeleteProgram(shaderProgram); glfwTerminate(); return 0; }
运行结果:
我们只设置了三个点的值,但是出来的颜色 都是渐变的,原因如下:
这个图片可能不是你所期望的那种,因为我们只提供了3个颜色,而不是我们现在看到的大调色板。这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。
1,什么叫纹理?
纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。
2,什么叫纹理坐标?为什么需要纹理坐标
为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)
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, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上 };
3,什么叫采样?
纹理坐标在x和y,范轴上围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。
4,纹理环绕方式
当纹理坐标超出默认范围时,每个选项都有不同的视觉效果输出。我们来看看这些纹理图像的例子:
5,纹理坐标和纹理像素
Texture Pixel也叫Texel,你可以想象你打开一张
.jpg
格式图片,不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色。6,纹理过滤
GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
7,glTexParameteri函数的用法:
(1)设置环绕方式:
环绕方式 描述 GL_REPEAT 对纹理的默认行为。重复纹理图像。 GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。 GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
//设置缩小之后边缘的颜色 float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f }; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
(2)纹理过滤
GL_NEAREST:选择中心点最接近纹理坐标的那个像素
GL_LINEAR: 它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
(3)多级渐远纹理
过滤方式 描述 GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 GL_LINEAR_MIPMAP_LINEAR 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 GL_LINEAR_MIPMAP_NEAREST中linear和nearest控制的部分glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
注意:当纹理缩小的时候,像素直接就进选取就行,因为当0~1有1000个像素的时候,像素之间的色差不大
8,生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D);
当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。
然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。
或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
9,纹理单元
一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元,所以教程前面部分我们没有分配一个位置值。
(1)图像翻转代码
stbi_set_flip_vertically_on_load(true);
(2)步骤:(把纹理单元,纹理对象和采样器三者绑定起来)
1)激活纹理单元0号--->绑定纹理0号--->读取第0张图片
2)激活纹理单元1号--->绑定纹理1号--->读取第1张图片
3)告诉着色器,那个采样器属于几号纹理单元,代码如下
/* * GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D * 告诉OpenGL每个着色器 采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面 * ourTexture0着色器属于0号纹理单元 */ ourShader.use(); ourShader.setInt("ourTexture0", 0); ourShader.setInt("ourTexture1", 1);
具体实现:
片段着色器:
#version 330 core out vec4 FragColor; in vec3 ourColor; in vec2 TexCoord; uniform sampler2D ourTexture0; uniform sampler2D ourTexture1; void main() { FragColor = mix(texture(ourTexture0, TexCoord), texture(ourTexture1,TexCoord), 0.3) * vec4(ourColor, 1.0); }
顶点着色器:
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aColor; layout (location = 2) in vec2 aTexCoord; out vec3 ourColor; out vec2 TexCoord; void main() { gl_Position = vec4(aPos, 1.0); ourColor = aColor; TexCoord = aTexCoord; }
// sample_1_1_HelloWindow.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include
#include #include #include "Shader.h" #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" void framebuffer_size_callback(GLFWwindow* window, int width, int height);//用户调整窗口大小之后的回调 void processInput(GLFWwindow* window);//实现输入控制 int drawRect();//画一个矩形 int drawTriangle(); int main() { drawRect(); return 0; } //用户调整窗口大小之后的回调 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } //实现输入控制 void processInput(GLFWwindow* window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//检查用户是否按下了返回键(Esc) { glfwSetWindowShouldClose(window, true); } } int drawRect() { glfwInit(); //初始化GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //主版本号(Major) glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号(Minor) glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //核心模式(Core-profile) GLFWwindow* window = glfwCreateWindow(800, 600, "learnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window!\n" << std::endl; glfwTerminate(); return -1; } /*创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了*/ glfwMakeContextCurrent(window); /*glad:加载所有OpenGL的函数指针*/ if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } /*告诉OpenGL渲染窗口的大小*/ glViewport(0, 0, 800, 600); /*设置窗口大小变化之后的回调*/ glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); /*着色器*/ CShader ourShader("shader.vs", "shader.fs"); 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, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上 }; unsigned int indices[] = { // note that we start from 0! 0, 1, 3, // first Triangle 1, 2, 3 // second Triangle }; /* 使用glGenBuffers函数和一个缓冲ID生成一个VBO对象*/ unsigned int VBO = 0; unsigned int VAO = 0; unsigned int EBO = 0; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); /* * OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。 * OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用 * glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上 * 任何对GL_ARRAY_BUFFER的操作,都是对VBO的操作 * 把之前定义的顶点数据复制到缓冲的内存 */ glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); unsigned int texture0, texture1; glActiveTexture(GL_TEXTURE0); //激活一个纹理单元 glGenTextures(1, &texture0); //生成纹理 glBindTexture(GL_TEXTURE_2D, texture0); //绑定纹理 /*为当前绑定的纹理对象设置环绕、过滤方式*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINES); /*纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINE); /*加载并生成纹理*/ int width, height, nrChannels; /*图像翻转*/ stbi_set_flip_vertically_on_load(true); unsigned char* data = stbi_load("yun.png", &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); /*会为当前绑定的纹理自动生成所有需要的多级渐远纹理*/ glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); glActiveTexture(GL_TEXTURE1); //激活一个纹理单元 glGenTextures(1, &texture1); //生成纹理 glBindTexture(GL_TEXTURE_2D, texture1); //绑定纹理 /*为当前绑定的纹理对象设置环绕、过滤方式*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINES); /*纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINE); /*加载并生成纹理*/ 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); /* * 当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。 * 这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否 * 则它就没有这个EBO配置了。 */ glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); /*画成线条*/ //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); /* * GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D * 告诉OpenGL每个着色器 采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面 * ourTexture0着色器属于0号纹理单元 */ ourShader.use(); ourShader.setInt("ourTexture0", 0); ourShader.setInt("ourTexture1", 1); /* * 渲染循环 * 在窗口结束之前,不断绘制图像,并且能够接收用户的输入 */ while (!glfwWindowShouldClose(window)) { /*输入*/ processInput(window); /* 渲染指令 激活,在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象 (也就是之前写的着色器)了。 */ glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glBindVertexArray(VAO); //glDrawArrays(GL_TRIANGLES, 0, 3); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); /*检查并调用事件,交换缓冲*/ glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glfwTerminate(); return 0; }
1,什么是向量?
2,向量的计算
3,矩阵计算
注意:建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会(消极地)互相影响。比如,如果你先位移再缩放,位移的向量也会同样被缩放(译注:比如向某方向移动2米,2米也许会被缩放成1米)!
应用
关于旋转,位移,放缩的顺序总结一下就是:
后者的计算可以影响到前一步的计算
代码:
顶点着色器:#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aColor; layout (location = 2) in vec2 aTexCoord; out vec3 ourColor; out vec2 TexCoord; uniform mat4 transform; void main() { gl_Position = transform * vec4(aPos, 1.0); ourColor = aColor; TexCoord = aTexCoord; }
片段着色器:
#version 330 core out vec4 FragColor; in vec3 ourColor; in vec2 TexCoord; uniform sampler2D ourTexture0; uniform sampler2D ourTexture1; uniform float ratio; void main() { FragColor = mix(texture(ourTexture0, vec2(1-TexCoord.x, TexCoord.y)), texture(ourTexture1,vec2(TexCoord.x, TexCoord.y)), ratio); }
// sample_1_1_HelloWindow.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include
#include #include #include "Shader.h" #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #include #include #include void framebuffer_size_callback(GLFWwindow* window, int width, int height); /*用户调整窗口大小之后的回调*/ void processInput(GLFWwindow* window);/*实现输入控制*/ int drawRect();/*画一个矩形*/ int drawTriangle(); float ratio = 0.5f; int main() { drawRect(); return 0; } //用户调整窗口大小之后的回调 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } //实现输入控制 void processInput(GLFWwindow* window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//检查用户是否按下了返回键(Esc) { glfwSetWindowShouldClose(window, true); } if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) { ratio += 0.01; if (ratio > 1) { ratio = 1; } } if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) { ratio -= 0.01; if (ratio <0) { ratio = 0; } } } int drawRect() { glfwInit(); //初始化GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //主版本号(Major) glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号(Minor) glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //核心模式(Core-profile) GLFWwindow* window = glfwCreateWindow(800, 600, "learnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window!\n" << std::endl; glfwTerminate(); return -1; } /*创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了*/ glfwMakeContextCurrent(window); /*glad:加载所有OpenGL的函数指针*/ if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } /*告诉OpenGL渲染窗口的大小*/ glViewport(0, 0, 800, 600); /*设置窗口大小变化之后的回调*/ glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); /*着色器*/ CShader ourShader("shader.vs", "shader.fs"); 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, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上 }; unsigned int indices[] = { // note that we start from 0! 0, 1, 3, // first Triangle 1, 2, 3 // second Triangle }; /* 使用glGenBuffers函数和一个缓冲ID生成一个VBO对象*/ unsigned int VBO = 0; unsigned int VAO = 0; unsigned int EBO = 0; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); /* * OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。 * OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用 * glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上 * 任何对GL_ARRAY_BUFFER的操作,都是对VBO的操作 * 把之前定义的顶点数据复制到缓冲的内存 */ glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); unsigned int texture0, texture1; glActiveTexture(GL_TEXTURE0); //激活一个纹理单元 glGenTextures(1, &texture0); //生成纹理 glBindTexture(GL_TEXTURE_2D, texture0); //绑定纹理 /*为当前绑定的纹理对象设置环绕、过滤方式*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINES); /*纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); /*加载并生成纹理*/ int width, height, nrChannels; /*图像翻转*/ stbi_set_flip_vertically_on_load(true); unsigned char* data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); /*会为当前绑定的纹理自动生成所有需要的多级渐远纹理*/ glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); glActiveTexture(GL_TEXTURE1); //激活一个纹理单元 glGenTextures(1, &texture1); //生成纹理 glBindTexture(GL_TEXTURE_2D, texture1); //绑定纹理 /*为当前绑定的纹理对象设置环绕、过滤方式*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINES); /*纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINE); /*加载并生成纹理*/ 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); /* * 当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。 * 这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否 * 则它就没有这个EBO配置了。 */ glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); /*画成线条*/ //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); /* * GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D * 告诉OpenGL每个着色器 采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面 * ourTexture0着色器属于0号纹理单元 */ ourShader.use(); ourShader.setInt("ourTexture0", 0); ourShader.setInt("ourTexture1", 1); /* * 渲染循环 * 在窗口结束之前,不断绘制图像,并且能够接收用户的输入 */ while (!glfwWindowShouldClose(window)) { /*输入*/ processInput(window); ourShader.setFloat("ratio", ratio); /* 渲染指令 激活,在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象 (也就是之前写的着色器)了。 */ glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glBindVertexArray(VAO); /*先缩放,再旋转,最后位移,显示出来就是trans先位移,再旋转,最后缩放 */ glm::mat4 trans = glm::mat4(1.0f); trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f)); trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0, 0.0, 1.0)); trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5)); unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform"); glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans)); //glDrawArrays(GL_TRIANGLES, 0, 3); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); /*第二个三角形*/ trans = glm::mat4(1.0f); trans = glm::translate(trans, glm::vec3(-0.5f, 0.5f, 0.0f)); //trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0, 0.0, 1.0)); float scaleValue = glm::sin(glfwGetTime()); trans = glm::scale(trans, glm::vec3(scaleValue, scaleValue, scaleValue)); transformLoc = glGetUniformLocation(ourShader.ID, "transform"); glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans)); //glDrawArrays(GL_TRIANGLES, 0, 3); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); /*检查并调用事件,交换缓冲*/ glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glfwTerminate(); return 0; }
将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
它们之间是如何变换的?
什么叫齐次方程,怎么用?
什么是齐次坐标? - 知乎要明白齐次方程是一个工具,为了解决现实世界两条平行的能够在远处相交的问题。
glm实现的代码:(注意使用顺序)
旋转:glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(1.0f, 1.0f, 0.0f));
位移:
glm::translate(model, cubePositions[i]);
放缩:
glm::scale(model, glm::vec3(0.5, 0.5, 0.5));
透视:
projection = glm::perspective(glm::radians(70.0f), 800.0f / 600.0f, 0.1f, 100.0f);
代码:
顶点着色器:
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aTexCoord; out vec2 TexCoord; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position =projection * view * model * vec4(aPos, 1.0); TexCoord = aTexCoord; }
片段着色器:
#version 330 core out vec4 FragColor; in vec2 TexCoord; uniform sampler2D ourTexture0; uniform sampler2D ourTexture1; uniform float ratio; void main() { FragColor = mix(texture(ourTexture0, vec2(TexCoord.x, TexCoord.y)), texture(ourTexture1,vec2(TexCoord.x, TexCoord.y)), ratio); }
实现:
// sample_1_1_HelloWindow.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include
#include #include #include "Shader.h" #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #include #include #include void framebuffer_size_callback(GLFWwindow* window, int width, int height); /*用户调整窗口大小之后的回调*/ void processInput(GLFWwindow* window);/*实现输入控制*/ int drawRect();/*画一个矩形*/ int drawTriangle(); float ratio = 0.5f; int main() { drawRect(); return 0; } //用户调整窗口大小之后的回调 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } //实现输入控制 void processInput(GLFWwindow* window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//检查用户是否按下了返回键(Esc) { glfwSetWindowShouldClose(window, true); } if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) { ratio += 0.01; if (ratio > 1) { ratio = 1; } } if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) { ratio -= 0.01; if (ratio <0) { ratio = 0; } } } int drawRect() { glfwInit(); //初始化GLFW glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //主版本号(Major) glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号(Minor) glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //核心模式(Core-profile) GLFWwindow* window = glfwCreateWindow(800, 600, "learnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window!\n" << std::endl; glfwTerminate(); return -1; } /*创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了*/ glfwMakeContextCurrent(window); /*glad:加载所有OpenGL的函数指针*/ if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } /*告诉OpenGL渲染窗口的大小*/ glViewport(0, 0, 800, 600); /*设置窗口大小变化之后的回调*/ glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); /*着色器*/ CShader ourShader("shader.vs", "shader.fs"); float vertices[] = { -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 0.5f, 0.5f, -0.5f, 1.0f, 1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f, 1.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 1.0f, 1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, 1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f }; unsigned int indices[] = { // note that we start from 0! 0, 1, 3, // first Triangle 1, 2, 3 // second Triangle }; /* 使用glGenBuffers函数和一个缓冲ID生成一个VBO对象*/ unsigned int VBO = 0; unsigned int VAO = 0; unsigned int EBO = 0; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); /* * OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。 * OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用 * glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上 * 任何对GL_ARRAY_BUFFER的操作,都是对VBO的操作 * 把之前定义的顶点数据复制到缓冲的内存 */ glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); unsigned int texture0, texture1; glActiveTexture(GL_TEXTURE0); //激活一个纹理单元 glGenTextures(1, &texture0); //生成纹理 glBindTexture(GL_TEXTURE_2D, texture0); //绑定纹理 /*为当前绑定的纹理对象设置环绕、过滤方式*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINES); /*纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); /*加载并生成纹理*/ int width, height, nrChannels; /*图像翻转*/ stbi_set_flip_vertically_on_load(true); unsigned char* data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); /*会为当前绑定的纹理自动生成所有需要的多级渐远纹理*/ glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); glActiveTexture(GL_TEXTURE1); //激活一个纹理单元 glGenTextures(1, &texture1); //生成纹理 glBindTexture(GL_TEXTURE_2D, texture1); //绑定纹理 /*为当前绑定的纹理对象设置环绕、过滤方式*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_LINES); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINES); /*纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。*/ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINE); /*加载并生成纹理*/ 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); /* * 当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。 * 这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否 * 则它就没有这个EBO配置了。 */ glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); /*画成线条*/ //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); /* * GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D * 告诉OpenGL每个着色器 采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面 * ourTexture0着色器属于0号纹理单元 */ ourShader.use(); ourShader.setInt("ourTexture0", 0); ourShader.setInt("ourTexture1", 1); /* * 渲染循环 * 在窗口结束之前,不断绘制图像,并且能够接收用户的输入 */ /*启用深度测试*/ glEnable(GL_DEPTH_TEST); glm::vec3 cubePositions[] = { glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(2.0f, 5.0f, -15.0f), glm::vec3(-1.5f, -2.2f, -2.5f), glm::vec3(-3.8f, -2.0f, -12.3f), glm::vec3(2.4f, -0.4f, -3.5f), glm::vec3(-1.7f, 3.0f, -7.5f), glm::vec3(1.3f, -2.0f, -2.5f), glm::vec3(1.5f, 2.0f, -2.5f), glm::vec3(1.5f, 0.2f, -1.5f), glm::vec3(-1.3f, 1.0f, -1.5f) }; while (!glfwWindowShouldClose(window)) { /*输入*/ processInput(window); ourShader.setFloat("ratio", ratio); /* 渲染指令 激活,在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象 (也就是之前写的着色器)了。 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glBindVertexArray(VAO); /*先缩放,再旋转,最后位移,显示出来就是trans先位移,再旋转,最后缩放 */ glm::mat4 view = glm::mat4(1.0f); view = glm::translate(view, glm::vec3(1.0f, -0.0f, -3.0f)); unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "view"); glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(view)); glm::mat4 projection = glm::mat4(1.0f); projection = glm::perspective(glm::radians(70.0f), 800.0f / 600.0f, 0.1f, 100.0f); transformLoc = glGetUniformLocation(ourShader.ID, "projection"); glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(projection)); for(unsigned int i = 0; i< 10; i++) { glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, cubePositions[i]); model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(1.0f, 1.0f, 0.0f)); //model = glm::scale(model, glm::vec3(0.5, 0.5, 0.5)); transformLoc = glGetUniformLocation(ourShader.ID, "model"); glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(model)); glDrawArrays(GL_TRIANGLES, 0, 36); } //glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); /*检查并调用事件,交换缓冲*/ glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glfwTerminate(); return 0; }
如何构建view矩阵:
这个图中的坐标是将世界坐标绕y轴旋转了180度
问:为什么要先乘位移矩阵再乘旋转矩阵呢?
因旋转矩阵将坐标轴一起旋转额,沿着x轴位移就变成了沿着旋转后的x轴位移了
view矩阵(观察矩阵):
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));
lookat函数的推导:
[OpenGL] 视图矩阵(View)矩阵与glm::lookAt函数源码解析_Zeehoy的博客-CSDN博客_glm::lookat
欧拉角:
滚转角不讨论是因为无论怎么旋转移动,都是z轴正方向对着我们(摄像机)