API文档参考:https://wikis.khronos.org/opengl/Category:Core_API_Reference

简介

  1. OpenGL:由Khronos组织指定的标准,可以对图形硬件设备特性进行访问,其接口由各个显卡厂商实现。
  2. 功能:完成模型数据从虚拟的三维世界,到屏幕上具体像素的渲染过程
  3. OpenGL的两种模式:
    1. 核心模式:即非立即渲染模式。可编程渲染管线,完全控制渲染流程。OpenGL3.2+中使用
    2. 立即渲染模式:这种方式灵活性低,使用固定渲染管线流程。OpenGl旧版本中使用

坐标系

  1. 坐标系有两种:
    1. 左手坐标系:左右手坐标系如下图所示:
      image
    2. 右手坐标系:OpenGL中使用的是右手坐标系

Mesh(网格)与材质

  1. Mesh:即网格,其存储了一个模型的几何形状数据。由多个三角形拼接而成,内部中空,包含顶点位置,法线等属性数据。
    1. 三角形:由三个顶点按照顺序构成
  2. 材质(Material):描述了物体表面如何与光发生反应,其决定了物体的颜色,凹陷还是突起,透明还是半透明,不透明等。
    1. 颜色:颜色可以分为红绿蓝三个通道表示
  3. 对于一个模型,mesh决定了它的形状,材质决定了它的样子

渲染管线

渲染流程如下:

  1. 顶点数据:每个顶点的位置(xyz坐标),颜色(rgb值),法线,UV(纹理坐标),切线等
  2. 三维变换
    1. 模型变换:使用矩阵使三角形顶点在空间当中平移,旋转,缩放
    2. 视图变换:使用矩阵使三角形顶点变化到以摄像机为中心的坐标系中
    3. 投影变换:使用矩阵使三角形顶点变换到标准屏幕坐标系中
  3. 图元装配:将变换后的顶点,根据顺序组成三角形,直线等图元的过程
  4. 剪裁剔除:将视口外面,无法显示的图元都裁剪掉,加快后续的渲染效率
  5. 光栅化:屏幕由一格格的像素构成,虚拟的几何图形需要做成栅格才能渲染,称为光栅化
  6. 片元着色:计算每个像素片元最终显示的颜色
  7. 混合与测试:颜色的混合决定了透明效果,测试决定像素的前后顺序

标准设备坐标系NDC与屏幕坐标系

  1. 标准化设备坐标NDC:使用-1到1之间的值表示顶点的坐标,本质就是比例值。
    1. 使用NDC的优势:与具体屏幕分辨率无关。有了NDC坐标,计算具体的屏幕像素坐标非常容易
  2. 屏幕坐标系

VBO

  1. VBO(Vertex Buffer Object):顶点缓存对象,表示在GPU显存中的一段存储空间对象,用于存储Mesh顶点属性数据。在C++程序中,表现为一个unsigned int类型的变量,可以理解为GPU端内存对象的一个ID编号
  2. 示例:
// 顶点数组,包含位置属性
float vertices[] = {
	   -0.5f, -0.5f, 0.0f,
		0.5f, -0.5f, 0.0f,
		0.0f,  0.5f, 0.0f
};

//创建一个vbo
GLuint vbo = 0;
glGenBuffers(1, &vbo);


//  binds a buffer object to the specified buffer binding point
glBindBuffer(GL_ARRAY_BUFFER, vbo);

// creates and initializes a buffer object's data store创建VBO并将内存中的数据拷贝到VBO
// 第三参数:how a buffer object's data store will be accessed
// GL_STATIC_DRAW:VBO模型数据不会频繁改变
// GL_DYNAMIC_DRAW:VBO模型数据会频繁改变
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);


glDeleteBuffers(1, &vbo);

VAO

  1. VAO(Vertex Array Object):顶点数组对象,用于存储一个Mesh网格所有的顶点属性描述信息,比如说位置信息描述,颜色信息描述,法线信息描述,UV信息描述等
    1. 每个顶点几个字节
    2. 每个字节的数据类型是什么
    3. 每个顶点的数据,步长是多少
    4. 当前属性的值在顶点数据内的偏移量(顶点可能包含位置属性,颜色属性等)
    5. 此属性的值存储在第几个VBO(比如说位置属性和颜色属性可以存储在不同的VBO)
  2. 示例:
//1 准备positions colors数据
float positions[] = {
	   -0.5f, -0.5f, 0.0f,
		0.5f, -0.5f, 0.0f,
		0.0f,  0.5f, 0.0f
};
float colors[] = {
   1.0f, 0.0f, 0.0f,
   0.0f, 1.0f, 0.0f,
   0.0f,  0.0f, 1.0f
};


//2 使用数据生成两个vbo posVbo, colorVbo
GLuint posVbo, colorVbo;
glGenBuffers(1, &posVbo);
glGenBuffers(1, &colorVbo);

glBindBuffer(GL_ARRAY_BUFFER, posVbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);

glBindBuffer(GL_ARRAY_BUFFER, colorVbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

//3 生成vao并且绑定
GLuint vao = 0;
// generate vertex array object names
glGenVertexArrays(1, &vao);
// bind a vertex array object
glBindVertexArray(vao);

//4.1描述位置属性
glBindBuffer(GL_ARRAY_BUFFER, posVbo);
// 激活VAO的0号位置
glEnableVertexAttribArray(0);
// 向VAO的0号位置中加入位置属性描述信息,最后一个参数0表示当前位置属性在顶点数据内的偏移量为0
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

//4.2 描述颜色属性
glBindBuffer(GL_ARRAY_BUFFER, colorVbo);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

glBindVertexArray(0);

Shader

1.GLSL

GLSL(Graphic Library Shader Language):一种类C语言

  1. Shader中的变量:
    1. 输入变量:称为属性变量,其值通过VAO描述,从VBO中读取的顶点属性值
    2. 输出变量:提供给后一动作使用
    3. Uniform变量:被当前shader运行的所有运算单元共享的变量,称为Uniform变量。在GLSL中使用uniform关键字定义Uniform变量,在OpenGL中对Uniform变量的相关操作如下所示:
    // 以下操作一般每一帧都需要进行操作
    
    
    // 1.通过Uniform变量名称拿到其位置,name是定义的Uniform变量名
    GLint location = glGetUniformLocation(mProgram, name.c_str());
    
    //2 通过Location更新Uniform变量的值
    glUniform1f(location, value);
    
    1. 内置变量:比如说gl_Position,gl_FragCoord
      1. gl_FragCoord:这是一个三维向量,XY表示当前绘制的片元在屏幕上的像素位置,Z记录了当前像素的深度值(距离摄像机的远近程度),范围为0~1
  2. Shader中的函数:
    1. 内置函数:
      1. 内置函数dFdx与dFdy:对栅格的某一个属性求偏导数(偏导数即为x或者y方向上的变化量)
Shader的分类

Shader:运行在GPU端的着色器程序,用于处理顶点数据以及决定像素片元最终着色。着色器使用GLSL编写

  1. VertexShader:处理顶点,进行三维变换,屏幕投影等各种操作
  2. FragmentShader:处理像素,决定着像素的颜色和纹理
  3. shader程序的编译和链接示例程序如下:
//1 定义vs与fs的源代码,并且装入字符串
const char* vertexShaderSource =
	"#version 460 core\n"
	"layout (location = 0) in vec3 aPos;\n" // 0表示在VAO的第0个属性描述中取数据
	"void main()\n"
	"{\n"
	"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" // gl_Position为内置变量
	"}\0";
const char* fragmentShaderSource =
	"#version 460 core\n"
	"out vec4 FragColor;\n"
	"void main()\n"
	"{\n"
	"   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
	"}\n\0";


//2 创建Shader程序(vs、fs)
GLuint vertex, fragment;
vertex = glCreateShader(GL_VERTEX_SHADER);
fragment = glCreateShader(GL_FRAGMENT_SHADER);


//3 为shader程序输入shader代码
glShaderSource(vertex, 1, &vertexShaderSource, NULL);
glShaderSource(fragment, 1, &fragmentShaderSource, NULL);

int success = 0;
char infoLog[1024];
//4 执行shader代码编译 
glCompileShader(vertex);
//检查vertex编译结果
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if (!success) {
	glGetShaderInfoLog(vertex, 1024, NULL, infoLog);
	std::cout << "Error: SHADER COMPILE ERROR --VERTEX" << "\n" << infoLog << std::endl;
}

glCompileShader(fragment);
//检查fragment编译结果
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
if (!success) {
	glGetShaderInfoLog(fragment, 1024, NULL, infoLog);
	std::cout << "Error: SHADER COMPILE ERROR --FRAGMENT" << "\n" << infoLog << std::endl;
}

//5 创建一个Program壳子
GLuint program = 0;
program = glCreateProgram();

//6 将vs与fs编译好的结果放到program这个壳子里
glAttachShader(program, vertex);
glAttachShader(program, fragment);

//7 执行program的链接操作,形成最终可执行shader程序
glLinkProgram(program);

//检查链接错误
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
	glGetProgramInfoLog(program, 1024, NULL, infoLog);
	std::cout << "Error: SHADER LINK ERROR " << "\n" << infoLog << std::endl;
}

//清理
glDeleteShader(vertex);
glDeleteShader(fragment);

EBO

  1. EBO(Element Buffer Object):用于存储顶点绘制顺序索引号的GPU显存区域
  2. 示例如下所示:
//1 准备positions
float positions[] = {
	-0.5f, -0.5f, 0.0f,
	0.5f, -0.5f, 0.0f,
	0.0f,  0.5f, 0.0f,
	0.5f,  0.5f, 0.0f,
};

unsigned int indices[] = {
	0, 1, 2,
	2, 1, 3
};

//2 VBO创建
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);

//3 EBO创建
GLuint ebo;
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

//4 VAO创建
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);

//5 绑定vbo ebo 加入属性描述信息
//5.1 加入位置属性描述信息
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, (void*)0);

//5.2 加入ebo到当前的vao
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);

glBindVertexArray(0);

绘制

1.绘制流程
  1. 指定绘制时所使用的Shader程序
  2. 指定绘制时所使用的VAO几何信息
  3. 调用绘制接口向GPU发出渲染指令
2.绘制接口

几个核心绘制API:

  1. glDrawArrays:
    1. 参数mode:指定绘制模式
      1. GL_TRIANGLES:每三个顶点构成一个三角形
      2. GL_TRIANGLE_STRIP:如果末尾点序号为偶数,则链接规则为n-2 n-1 n;如果末尾点序号为奇数,则链接规则为n-1 n-2 n
      3. GL_TRIANGLE_FAN:绘制为扇形序列,所有顶点都和0号顶点存在关系,比如说4个顶点,则012,023
      4. GL_LINES:每两个顶点构成一条直线
      5. 等等
  2. glDrawElements:
    1. indices:
      1. 如果使用了EBO,通常写0
      2. 如果使用了EBO,传入非0表示索引内偏移
      3. 如果不使用EBO,则可直接传入索引数组

纹理Texture

1.纹理
  1. 纹理贴图:在绘制三角形的过程中,将图片贴到三角形上进行显示的过程即纹理贴图。纹理贴图是一种将图像或者纹理应用到3D模型表面的技术
  2. 纹理对象:在GPU端,用来以一定格式存放纹理图片描述信息与数据信息的对象
  3. 纹理单元:在OpenGL中,用于链接采样器和纹理对象,以便让采样器知道从哪一个纹理对象进行采样
  4. 纹理过滤:
    1. 邻近过滤:根据UV坐标计算出来的小数坐标,取得最近整数,得到像素。这种方法的问题是由于图片像素不够,就会有多个像素被复用,导致了像素感偏重。如果纹理分辨率 > 屏幕分辨率,则可以使用这种方法
    2. 双线性插值过滤:根据UV坐标计算出来的小数坐标,综合周边像素得到新像素颜色。如果纹理分辨率 < 屏幕分辨率,则可以使用这种方法
    3. 在OpenGl中使用方法glTextureParameteri进行设置
    // GL_TEXTURE_MAG_FILTER表示纹理分辨率 < 屏幕分辨率,控制如何采样
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // GL_TEXTURE_MIN_FILTER表示纹理分辨率 > 屏幕分辨率,控制如何采样
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    
  5. 纹理包裹:当UV坐标超出了0-1的范围,则
    1. Repeat:重复纹理
    2. Mirrored:镜像纹理
    3. ClampToEdge:边缘复用
    4. ClampToBorder:设置边缘颜色,且复用
    5. 在OpenGL中使用glTextureParameteri方法进行设置
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);//u
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);//v
    
  6. 纹理混合
2.采样
  1. 图片采样:在进行屏幕上某一像素绘制时,根据UV坐标以一定算法,决定使用纹理图片上某个像素颜色的过程,即为采样。
  2. 采样器:在GPU端,执行采样的对象
3.UV坐标
  1. UV坐标:用于表达当前像素或者顶点对应纹理图片上百分比位置的横纵百分比,称之为UV坐标。比如说(0,0)对应纹理图片的左下角,(1,1)对应纹理图片的右上角
  2. UV坐标的用途:在绘制三角形时,已知每一个顶点的UV坐标。在光栅化时,通过重心插值算法计算每个像素的UV坐标,根据UV坐标乘以图片的宽高采样图片上的像素颜色,填充在buffer(像素数据缓冲区)里进行显示
4.MipMap

为一张纹理图片生成一系列纹理图像,后一个图像是前一个的二分之一。当物体距离摄像机较远的时候,会不断切换为更小的贴图进行使用。

  1. 通过原始图片生成多级MipMap,0号级别MipMap对应纹理自身,1号级别纹理对应(texture.width/ 2, texture.height / 2),以此类推:步骤如下
    1. 滤波:对图片进行模糊预处理。滤波方法有:
      1. 均值滤波
      2. 高斯滤波等
    2. 采样:对模糊的图片,选取像素组成下一级MipMap。采样方式有:
      1. 二分下采样
      2. 计算统计值
  2. 如何判断当前物体使用哪一级别的MipMap:通过glsl中的求偏导函数计算变化量决定。对于1个像素,对应m*n个纹素(纹理图片上的像素),则MipMap的级别可以为log2(max(m,n))
    1. 如何得知当前物体的像素,对应多少个纹素
    // 1.获得当前物体像素对应纹理上的纹素的具体位置
    vec2 location = uv * vec2(textureWidth, textureHeight);
    // 2.计算当前像素对应纹素具体位置相对x和y方向的变化量。
    vec2 dx = dFdx(location);
    vec2 dy = dFdy(location);
    // 3.选择最大的变化量delta,求log2(delta)的值,dot表示向量的点乘运算
    
    // 假设当前像素对应m*n个纹素,则maxDelta为m与n的最大值
    // maxDelta < 1,L < 0 ,此时图片放大了。此时不考虑使用MipMap
    // 1 < maxDelta < 2,0 < L < 1 ,L超过0.5就取1级,小于0.5就取0级
    // maxDelta = 2,L = 1 ,1个像素对应2个纹素。此时选取1号级别的MipMap
    // maxDelta = 4,L = 2 ,1个像素对应4个纹素。此时选取2号级别的MipMap
    float maxDelta = sqrt(max(dot(dx,dx),dot(dy,dy)));
    float L = log2(maxDelta);
    
    // 计算出MipMap级别,并且采样
    int level = max(int(L + 0.5), 0);
    // 使用对应级别的MipMap进行采样
    FragColor = textureLod(sampler,uv, level);
    
  3. 如何判断当前物体贴图,是放大还是缩小
5.OpenGl中实现MipMap
  1. 手动实现:将每个层级的MipMap数据通过glTexImage2D,设置到OpenGL状态机中
  2. 自动实现:
// glTexImage2D将纹理数据从cpu传送到GPU后调用以下方法生成MipMap
glGenerateMipmap(GL_TEXTURE_2D);

GLM库的使用

这个库实现了很多数学运算,这里版本使用的是GLM1.0.1

  1. 使用这个库可以完成屏幕上三角形的旋转,平移,缩放等操作
  2. 注意:glm中的旋转以本地坐标系的中点为基准;glm中的平移变换永远以本地坐标系中的坐标轴为准

深度检测

  1. 深度值:表示当前绘制的片元距离摄像机远近的程度
    image
  2. 深度值插值:每个顶点得到深度值后,会插值到所有的片元上
  3. 深度检测:通过将每一个片元的深度值进行记录和对比,来决定是否被覆盖
  4. 深度缓存:分配一块与颜色画布长宽一致的第二张画布,里面记录了每个像素当前距离相机最近的深度值
  5. 深度测试开启与配置:OpenGL中,在默认的画布系统中,存在一张深度缓存画布,可以通过下面配置进行开启使用
    1. 开启深度缓存功能
    glEnable(GL_DEPTH_TEST);
    
    1. 设置深度测试方法
    深度测试方法有以下:
    GL_LESS: 当前片元深度值较小的时候,才能通过测试
    
    GL_GREATER: 当前片元深度值较大的时候,才能通过测试
    
    GL_EQUAL: 当前片元深度值与缓存相同的时候,才能通过测试
    
    GL_LEQUAL: 当前片元深度值小于等于缓存的时候,才能通过测试
    
    GL_GEQUAL: 当前片元深度值大于等于缓存的时候,才能通过测试
    
    GL_NOTEQUAL: 当前片元深度值不等于缓存的时候,才能通过测试
    
    GL_NEVER: 永远不通过测试
    
    GL_ALWAYS: 总是通过测试(默认)
    glDepthFunc(GL_LESS);
    
    1. 清理深度缓存:进行帧更新时需要调用
      1. 方式1:使用glClear方法配上GL_DEPTH_BUFFER_BIT宏
      // 画布清理(画布颜色值及画布深度值清理)
      // 默认深度缓存值被清理为1.0
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      
      1. 方式2:
      glClearDepth(depth);
      
  6. Z-Fighting:当两个面片相邻过于接近的时候,由于深度缓冲数据精度不够,会出现无法判断谁先谁后的问题。
    image
    1. 建模时不要将物体摆设太近
    2. 使用glPolygonOffset对物体进行调配,这个函数可以用于调整单个模型深度值的大小。深度斜率的定义:深度值在屏幕空间变化的速度(xy方向上的最大值)
      image

几何体的构造

封装一些常见的几何体,比如说球体,立方体。封装时将顶点的位置属性,颜色属性,uv属性等放置在VBO中,由一个VAO描述。对外提供绘制接口,获取VAO接口,获取EBO中存放的索引数量接口。

立方体
  1. 确定了立方体的边长,就知道每个顶点的坐标
  2. 设定立方体中心位于世界坐标系原点
经纬球
  1. 顶点的位置计算:
    image

note:学完OpenGL基础,应该知道以下概念:

  1. 渲染管线
  2. VBO:顶点缓存对象,用于存放定义的顶点属性信息,顶点属性信息包括位置,法线,颜色,UV等
  3. VAO:顶点数组对象,用于存放顶点属性信息的描述信息
  4. EBO:用于存储顶点绘制顺序(存放顶点的索引号)
  5. 三大矩阵:模型变换矩阵,视图变换矩阵,投影变换矩阵
  6. shader:一种使用GLSL语言编写然后编译链接形成的着色器程序