OpenGl
- 渲染管线的主要功能:决定在给定虚拟摄像机、三维物体、光源、照明模式,以及纹理等诸多条件的情况下生成或绘制一幅二维图像的过程。
- GPU渲染流水线(渲染管线)的三个阶段
- 应用阶段:将需要绘制出来的几何体图元(点、线、矩、阵)输入到绘制管线的下一个阶段
- 具体包括图元的顶点数据、摄像机位置、光照纹理等参数
- 几何阶段:将顶点数据最终进行屏幕映射
- 将各个图元放入到世界坐标系统,也就是进行模型变换
- 根据光照纹理等计算顶点处材质的光照着色效果
- 根据摄像机的位置、取景范围进行观察变换和裁剪
- 最后进行屏幕映射,将三维模型转换到屏幕坐标系中
- 光栅化阶段:输出到屏幕的各个像素值
- 应用阶段:将需要绘制出来的几何体图元(点、线、矩、阵)输入到绘制管线的下一个阶段
OpenGl管线
计算机图形系统
- 计算机图形系统的组成
- 图形硬件
- 输入设备
- 输出设备
- 显示设备
- 计算机
- 图形软件
- 图形硬件
程序流程
- 初始化:初始化GLFW和GLAD
- 数据处理:生成VBO和VAO 发送数据到GPU 设置属性指针 告诉GPU如何解释数据
- 着色器:顶点和片段着色器(对数据进行处理)
- 渲染
- 善后工作
-
EBO:Element Buffer Object 索引缓冲区对象,主要用来存储顶点的索引信息
- 如果不使用索引直接用点坐标表示,绘制一个四边形需要 [(0,0), (2,0), (1,2), (1,2), (2,0), (3,2)]
- 如果使用索引 0->(0,0) 1->(2,0) 2->(1,2) 3->(3,2),使用[0,1,2,2,1,3]即可表示
-
VBO:Vertex Buffer Object 顶点缓冲区数据,主要用来存储顶点的各种信息
- 模型的顶点信息写入VBO,每次绘制模型不用劳烦CPU了,可以直接从GPU的显存中获得
-
VAO:Vertex Array Object 顶点数组对象,保存了所有顶点数据属性的状态集合,存储了顶点数据的格式以及顶点数据所需的VBO对象的引用(VAO相当于对多个VBO的引用)
glut glfw glew freeglut glad gl3w 都是不同的东西
glut 已经废弃
freeGlut 替代glut
glfw 用于台式机的OpenGL,OpenGL ES和Vulkan开发的开源,多平台库
glew OpenGL2.0之后的一个工具函数
glad glew的升级版
- OpenGL相当于一个大的状态机,设置之后整体改变为使用当前的状态
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <math.h>
#include <cstring>
#pragma comment(lib,"gltools.lib")
int main(int argc, char* argv[])
{
glfwInit(); // 初始化glfw
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // OpenGL版本为3.3 主版本号为设置为3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // OpenGL版本为3.3 主版本号设置为3
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心模式 无需后向兼容性
//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // MacOs 加上
glfwWindowHint(GLFW_RESIZABLE, false); // 不可更改窗口大小
// 设置宽高
int screen_width = 1280;
int screen_height = 720;
// 设置窗口
auto window = glfwCreateWindow(screen_width, screen_height, "Computer", nullptr, nullptr);
// 如果窗口创建失败 输出Failed to create OpenGL Context
if (window == nullptr)
{
std::cout << "Failed to create OpenGL Context" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window); // 将窗口的上下文设置为当前线程的主上下文
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) // 初始化GLAD
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
glViewport(0, 0, screen_width, screen_height); // 创建视口
/// 上面生成创建 窗口 下面处理数据 设置属性指针
// 三角形的顶点数据 数据需要在-1~1之间
const float triangle[] = {
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f, // 右下
0.0f, 0.5f, 0.0f // 正上
};
// 生成和绑定VBO和VAO
GLuint vertext_array_object; // VAO 可以一次性发送多个数据 而不用一个个发送
glGenVertexArrays(1, &vertext_array_object);// 生成
glBindVertexArray(vertext_array_object); // 绑定
GLuint vertext_buffer_object; // VBO
glGenBuffers(1, &vertext_buffer_object); // 生成缓冲区
glBindBuffer(GL_ARRAY_BUFFER, vertext_buffer_object); // 绑定
// 将顶点数据绑定到默认的缓冲中
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);
// 设置顶点属性指针 告诉GPU如何解释顶点数据
// 0 顶点着色器的位置值(这里没有);3 顶点属性是三分量的向量 ;GL_FLOAT 顶点类型
// GL_FALSE 表示是否希望数据标准化即映射到0~1 ;3 * sizeof(float) 步长,连续顶点属性之间的间隔,这里表示下一个顶点的数据在3个float之后 ;(void*) 0 数据的偏移量,因为给定数组一开始就是数据,所以偏移量为0,注意需要强转类型
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 开启通道 默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,意味着数据在着色器端是不可见的
// 设置属性指针完成后 需要解绑VBO和VAO
// 1. 防止之后绑定的VAO VBO影响当前的VAO VBO
// 2. 使代码灵活规范 在需要渲染的时候绑VAO 然后绘制
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 使用顶点和片段着色器 着色器源码 -> 生成并编译着色器 -> 链接着色器到着色器程序 -> 删除着色器
const char* vertext_shader_source = // 顶点着色器这里其实什么都没做
"#version 330 core\n"
"layout(location = 0) in vec3 aPos;\n"// 位置变量的属性位置值为0
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
"}\n\0";
const char* fragment_shader_source = // 片段着色器 设置颜色rgba = (1.0f, 0.5f, 0.2f, 1.0f)
"#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";
// 编译着色器
// 顶点着色器
int vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader, 1, &vertext_shader_source, NULL);
glCompileShader(vertex_shader);
int success = -1;
char info_log[512];
// 检查着色器是否成功过编译,如果失败打印错误信息
glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
std::cout << "Error::Shader::Vertext::Compilation_failed\n" << info_log << std::endl;
}
// 片段着色器
int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
glCompileShader(fragment_shader);
memset(info_log, 0, sizeof(info_log));
// 检查着色器是否成功过编译,如果失败打印错误信息
glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
std::cout << "Error::Shader::Fragnebt::Compilation_failed\n" << info_log << std::endl;
}
// 链接顶点和片段着色器至一个着色器程序中
int shader_program = glCreateProgram();
glAttachShader(shader_program, vertex_shader); //添加顶点着色器
glAttachShader(shader_program, fragment_shader); //添加片段着色器
glLinkProgram(shader_program);
// 检查链接是否成功 不成功打印错误信息
glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
if (!success)
{
memset(info_log, 0, sizeof(info_log));
glGetProgramInfoLog(shader_program, 512, NULL, info_log);
std::cout << "Error::Shader::Program::Compilation_failed\n" << info_log << std::endl;
}
// 然后就可以删掉着色器了 只需要使用编译好的program就可以了 着色器就不需要了
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);
// 开启线框模式 可以解开注释看看效果
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
while (!glfwWindowShouldClose(window))
{
// 清空颜色缓冲
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // 使用黑色填充(最顶层的背景颜色)
glClear(GL_COLOR_BUFFER_BIT);
// 使用着色器程序
glUseProgram(shader_program);
// 绘制三角形
glBindVertexArray(vertext_array_object); // 绑定VAO
glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形
glBindVertexArray(0); // 解除绑定
// 交换缓冲并检查是否有触发事件(键盘鼠标等)
glfwSwapBuffers(window);
glfwPollEvents();
}
// 删除VAO和VBO
glDeleteVertexArrays(1, &vertext_array_object);
glDeleteBuffers(1, &vertext_buffer_object);
// 清理所有的资源并正确退出程序
glfwTerminate();
return 0;
}
图形学:Bresenham算法绘制直线、圆、椭圆等
X-扫描线思想、Y向连贯性算法
- 三维几何变换
- glTranslate*()
- glRotate*()
- glScale*()
- 投影
- glFrustum()
- gluPerspective()
- glOrtho()
- 窗口裁剪
- 视口变换
- 投影的三个函数
- glViewport()
// display()
// 设置绘制相关的参数,完成绘制
glutDisplayFUnc(display)
// myReshape()
// 设置投影变换和视口变换的参数
glutReshapeFunc(myReshape)
// 键盘的回调函数
glutSpecialFunc(processSpecialKeys)
glutKeyboardFunc(processNormalKeys)
平移
- 针对各个顶点进行移动即可:调用函数 glTranslate(
Δ
\Delta
Δx,
Δ
\Delta
Δy,
Δ
\Delta
Δz)*
- 即设置P(x, y, z)
- 偏移向量T( Δ \Delta Δx, Δ \Delta Δy, Δ \Delta Δz)
- 平移后的坐标 P`(x`, y`, z`);
[ x ‘ y ‘ z ‘ 1 ] = [ 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 ] ∗ [ x y z 1 ] + [ Δ x Δ y Δ z 1 ] \begin{bmatrix} x` \\ y` \\ z` \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} + \begin{bmatrix} {\Delta}x \\ {\Delta}y \\ {\Delta}z \\ 1 \end{bmatrix} ⎣⎢⎢⎡x‘y‘z‘1⎦⎥⎥⎤=⎣⎢⎢⎡1000010000100001⎦⎥⎥⎤∗⎣⎢⎢⎡xyz1⎦⎥⎥⎤+⎣⎢⎢⎡ΔxΔyΔz1⎦⎥⎥⎤
缩放
- P(X,Y,Z)乘上一个放缩因子(
S
X
,
S
y
,
S
z
S_X, S_y, S_z
SX,Sy,Sz),得到P`(X`, Y`, Z`)
- 注意这里缩放是以锚点为原点进行的缩放,即缩放之后物体中心位置发生变化
- 为了应对上述的中心位置变化问题,在缩放之之前先将物体中心移动到原点再缩放,缩放之后再将物体移动回原来的位置
- Translate( − X p , − Y p , − Z p -X_p, -Y_p, -Z_p −Xp,−Yp,−Zp)
- Scale( S x , S y , S z S_x, S_y, S_z Sx,Sy,Sz)
- Tranlate( X p , Y p , Z p X_p, Y_p, Z_p Xp,Yp,Zp)
- 对应OpenGl函数 flScale*(TYPE Sx, TYPE Sy, Type Sz)
[ x ‘ y ‘ z ‘ 1 ] = [ S x 0 0 0 0 S y 0 0 0 0 S z 0 0 0 0 1 ] ∗ [ x y z 1 ] \begin{bmatrix} x` \\ y` \\ z` \\ 1 \end{bmatrix} = \begin{bmatrix} S_x & 0 & 0 & 0 \\ 0 & S_y & 0 & 0 \\ 0 & 0 & S_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} ⎣⎢⎢⎡x‘y‘z‘1⎦⎥⎥⎤=⎣⎢⎢⎡Sx0000Sy0000Sz00001⎦⎥⎥⎤∗⎣⎢⎢⎡xyz1⎦⎥⎥⎤
旋转
void glRotate*(TYPE angle, TYPE x, TYPE x, Type y, Type z);
- P点绕Z轴逆时针旋转 α ° \alpha° α°:使用极坐标运算
{ X = r ∗ cos ρ Y = r ∗ sin ρ 1 { X ‘ = r ∗ cos ( ρ + α ) = r ∗ cos ρ ∗ cos α − r ∗ sin ρ ∗ cos α Y ‘ = r ∗ sin ( ρ + α ) = r ∗ cos ρ ∗ sin α − r ∗ sin ρ ∗ cos α 2 将 1 式 带 入 2 式 得 到 { X ‘ = X ∗ cos α − Y ∗ sin α Y ‘ = X ∗ sin α + Y ∗ cos α \left\{ \begin{array}{c} X = r*\cos\rho \\ Y = r*\sin\rho \end{array} \right. 1 \\ \left\{ \begin{array}{c} X` = r*\cos(\rho+\alpha) = r*\cos\rho*\cos\alpha - r*\sin\rho*\cos\alpha \\ Y` = r*\sin(\rho+\alpha) = r*\cos\rho*\sin\alpha - r*\sin\rho*\cos\alpha \end{array} \right.2 \\ 将1式带入2式得到 \\ \left\{ \begin{array}{c} X` = X*\cos\alpha - Y*\sin\alpha \\ Y` = X*\sin\alpha + Y*\cos\alpha \end{array} \right. {X=r∗cosρY=r∗sinρ1{X‘=r∗cos(ρ+α)=r∗cosρ∗cosα−r∗sinρ∗cosαY‘=r∗sin(ρ+α)=r∗cosρ∗sinα−r∗sinρ∗cosα2将1式带入2式得到{X‘=X∗cosα−Y∗sinαY‘=X∗sinα+Y∗cosα
其他还有绕X轴、绕Y轴旋转
绕 Z 轴 旋 转 的 矩 阵 表 示 [ x ‘ y ‘ z ‘ 1 ] = [ cos α − sin α 0 0 sin α cos α 0 0 0 0 1 0 0 0 0 1 ] ∗ [ x y z 1 ] 绕 Y 轴 旋 转 的 矩 阵 表 示 [ x ‘ y ‘ z ‘ 1 ] = [ cos α 0 sin α 0 0 1 0 0 − sin α 0 cos α 0 0 0 0 1 ] ∗ [ x y z 1 ] 绕 X 轴 旋 转 的 矩 阵 形 式 [ x ‘ y ‘ z ‘ 1 ] = [ 1 0 0 0 0 cos α − sin α 0 0 sin α cos α 0 0 0 0 1 ] ∗ [ x y z 1 ] 绕Z轴旋转的矩阵表示\\ \begin{bmatrix} x` \\ y` \\ z` \\ 1 \end{bmatrix} = \begin{bmatrix} \cos\alpha & -\sin\alpha & 0 & 0 \\ \sin\alpha & \cos\alpha & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix}\\ 绕Y轴旋转的矩阵表示\\ \begin{bmatrix} x` \\ y` \\ z` \\ 1 \end{bmatrix} = \begin{bmatrix} \cos\alpha & 0 & \sin\alpha & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\alpha & 0 & \cos\alpha & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} \\ 绕X轴旋转的矩阵形式\\ \begin{bmatrix} x` \\ y` \\ z` \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\alpha & -\sin\alpha & 0 \\ 0 & \sin\alpha & \cos\alpha & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} 绕Z轴旋转的矩阵表示⎣⎢⎢⎡x‘y‘z‘1⎦⎥⎥⎤=⎣⎢⎢⎡cosαsinα00−sinαcosα0000100001⎦⎥⎥⎤∗⎣⎢⎢⎡xyz1⎦⎥⎥⎤绕Y轴旋转的矩阵表示⎣⎢⎢⎡x‘y‘z‘1⎦⎥⎥⎤=⎣⎢⎢⎡cosα0−sinα00100sinα0cosα00001⎦⎥⎥⎤∗⎣⎢⎢⎡xyz1⎦⎥⎥⎤绕X轴旋转的矩阵形式⎣⎢⎢⎡x‘y‘z‘1⎦⎥⎥⎤=⎣⎢⎢⎡10000cosαsinα00−sinαcosα00001⎦⎥⎥⎤∗⎣⎢⎢⎡xyz1⎦⎥⎥⎤
- 如何计算延任意向量(
A
x
,
A
y
,
A
z
A_x, A_y, A_z
Ax,Ay,Az)旋转
- 构建新的坐标系统 O X ‾ . Y ‾ . Z ‾ O\overline{X}.\overline{Y}.\overline{Z} OX.Y.Z并且以向量( A x , A y , A z A_x, A_y, A_z Ax,Ay,Az)为Z轴
- 将物体的点从原 O X Y Z OXYZ OXYZ坐标系统变到新的 O X ‾ . Y ‾ . Z ‾ O\overline{X}.\overline{Y}.\overline{Z} OX.Y.Z坐标系统中
- 变换坐标系统 O X ‾ . Y ‾ . Z ‾ O\overline{X}.\overline{Y}.\overline{Z} OX.Y.Z之后延Z轴旋转
- 旋转之后变换会原坐标系统 O X Y Z OXYZ OXYZ
[ x ‘ y ‘ z ‘ 1 ] = A − 1 [ cos α − sin α 0 0 sin α cos α 0 0 0 0 1 0 0 0 0 1 ] ∗ A ∗ [ x y z 1 ] \begin{bmatrix} x` \\ y` \\ z` \\ 1 \end{bmatrix} = A^{-1} \begin{bmatrix} \cos\alpha & -\sin\alpha & 0 & 0 \\ \sin\alpha & \cos\alpha & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * A * \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} ⎣⎢⎢⎡x‘y‘z‘1⎦⎥⎥⎤=A−1⎣⎢⎢⎡cosαsinα00−sinαcosα0000100001⎦⎥⎥⎤∗A∗⎣⎢⎢⎡xyz1⎦⎥⎥⎤
其中A为从原坐标系变为 O X ‾ . Y ‾ . Z ‾ O\overline{X}.\overline{Y}.\overline{Z} OX.Y.Z坐标系的变换矩阵
A − 1 A^{-1} A−1为A的逆矩阵,将坐标系统变回来,这里 A − 1 A^{-1} A−1也可以写为 A T A^{T} AT,因为 A A A是一个正交矩阵
GPU的配置就是适合矩阵运算,使用矩阵运算可以减少计算次数
glLoadIdentity(); 加载单位矩阵 随后可以进行后续变换 最后再作用到图元上
矩阵变换
// P1 经过变换T1,T2;P2经过变换T3,T4
glLoadIdentity();
Transformation T1;
Transformation T2;
Primitive P1;
glLoadIdentity();
Transformation T3;
Transformation T4;
Primitive P2;
- 使用堆栈管理矩阵
- glLoadIdentity(); 初始化单位矩阵,使
栈顶矩阵
为单位矩阵- 其他的平移、旋转、缩放操作也是修改
栈顶矩阵
- 其他的平移、旋转、缩放操作也是修改
- glPushMatrix(); 保存当前环境,将
栈顶矩阵
复制一份,入栈 - glPopMatrix(); 回复环境,普通
退栈
- glLoadIdentity(); 初始化单位矩阵,使
// 假设p1,p2有相同的变换Tc; p1又经过变换T1,T2;P2经过T3,T4变换
glLoadIdentity()
Transformation Tc;
glPushMatrix();
Transformation T1;
Transformation T2;
Primitive p1;
glPopMatrix();
glPushMatrix();
Transformation T3;
Transformation T4;
Primitive p2;
glPopMatrix();
所谓
glPushMatrix
和glPopMatrix
需要搭配出现,也可以进行嵌套
glPushMatrix();
glTranslatef(-1.0, 0.0, 0.0);
glRotatef((GLfloat)shoulder, 0.0, 0.0, 1.0);
glTranslatef(1.0, 1.0, 1.0);
glPushMatrix();
glScalef(2.0, 0.4, 1.0);
glutWireCube(1.0);
glPopMatrix();
glPushMatrix();
glTranslatef(1.0, 0.0, 0.0);
glRotatef((GLfloat)elbow, 0.0, 0.0, 1.0);
glTranslatef(1.0, 0.0, 0.0);
glScalef(2.0, 0.4, 1.0);
glutWireCube(1.0);
glPopMatrix();
glPopMatrix();
像这样搭配出现,保证被
glPushMatrix
和glPopMatrix
包裹的计算可以不用担心对其他矩阵有所影响
模型变换与视点变换
-
Model transformation 视点不变,变物体
-
View transformation 物体不变,变视点(视点可以理解为你的头,视点变换就是左右移动等)
-
glMatrixMode(GL_MODELVIEW)
设置矩阵模式,告诉GL_MODELVIEW程序后面要修改的是模型-视点变换要先申明变换 -
一般视点不变,模型变(移动,旋转,缩放)
-
void gluLookAt(eyeX, eyeZ, eyeZ, centerX, centerY, centerZ, upX, upY, upZ)
- 这个是专门进行视点变换的函数
- eye* 表示设置视点坐标;center* 表示设置观察点的坐标;up* 表示视点(头)向上的向量
全局变换与局部变换
- 每个物体都有一个自己的坐标系,跟着物体移动
- 比如机器人的手臂要旋转时,通过全局变换来计算是很麻烦的,但是通过手臂关节的局部坐标来变换就简单很多
- OpenGL中实现的
- 如果是单一旋转平移,是全局变换
- 如果是组合变换,是局部变换(全局变换的相反顺序就是局部变换)这里不好表述…得自己查了
投影
- 人眼就是透视投影,近大远小
- 平行投影
- 正投影(平行投影特殊情况)
// 三个跟投影相关的函数
// 透视投影 可以不对称
// 六个参数就是棱台体的左面、右面、上面、下面、靠近的前面、远距离的背面 棱台体处于Z轴的负方向 需要百度图片
void glFrustum(GLdouble left, GLdouble right, Gldouble bottom, GLdouble top, GLdouble near, GLdouble far);
// 透视投影 跟glFrustum一样,这个参数比较直观 glu库中的 只能是对称的
// fovy 张角视角 aspect 投影面比例 宽高比例 zNear 靠近的前面 zFar 远距离的背面
void gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar);
// 正投影
void glOrtho(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far);
// 当用户改变窗口大笑的时候调用的回调
void myReshape(GLsizei w, GLsizei h){
// 设置视区
glViewport(0, 0, w, h);
// 设定透视方式
glMatrixMode(GL_PROJECTION); // 改变矩阵模式为投影矩阵变换
glLoadIdentity();
gluPerspective(60.0, 1.0*(GLfloat)w/(GLfloat)h, 1.0, 30.0);
// gluPerspective(60.0, 1.0, 1.0, 30.0);
// glFrustum(-1.0, 1.0, -1.0, 1.0, 1.0, 30.0);
}
OpenGL中的使用
光照
-
通过打光给物体明暗变化,给物体立体感
-
光照明模型
- 环境光:光源发出的光在场景中经过多次反射,折射后还是会对物体产生影响。环境光就是反映这种周环境对物体的光照影响
- 漫反射光
- 高光
-
Phong光照模型:经验模型而非真实物理模型 I = K a ∗ I a + K d ∗ I l ∗ cos θ + K s ∗ L l ∗ cos n ϕ I=K_a*I_a+K_d*I_l*\cos\theta+K_s*L_l*\cos^n\phi I=Ka∗Ia+Kd∗Il∗cosθ+Ks∗Ll∗cosnϕ
- Diffuse reflection 漫反射
- Specular reflection 镜面反射
- Ambient light 环境光
- 环境光很复杂,Phong模型干脆只用一个常数表示
I
e
=
K
a
∗
I
a
I_e = K_a*I_a
Ie=Ka∗Ia
- I a I_a Ia环境光的亮度
- K a K_a Ka物体表面的环境光反射系数( K a ∈ [ 0 , 1 ] K_a\in[0,1] Ka∈[0,1])
- 漫反射默认粗糙表面的光会向四周均匀反射
I
=
K
d
∗
I
l
∗
cos
θ
I = K_d*I_l*\cos\theta
I=Kd∗Il∗cosθ
- K d K_d Kdd表示diffuse,漫射光的反射系数或反射率 K d ∈ [ 0 , 1 ] K_d\in[0,1] Kd∈[0,1]
- I l I_l Ill表示light
- 入射光角度变化时发射光变换不是根据 cos θ \cos\theta cosθ变化的,但是大致是一个递减的变换,所以为了简化计算所以使用 cos θ \cos\theta cosθ
- 镜面反射与视点位置有关
- I = K s ∗ I l ∗ cos n ϕ I=K_s*I_l*\cos^n\phi I=Ks∗Il∗cosnϕ
- K s K_s Ks高光的反射率
- I l I_l Il表示light
- cos n ϕ \cos^n\phi cosnϕ中n表示高光指数, ϕ \phi ϕ表示的不是入射角度,具体细节需要查
-
Blinn光照模型
-
三原色:红绿蓝(RGB): 对于不同的分量都可以设置不同的RGB即 K a R , K d R , K s R , I l R K_{aR},K_{dR}, K_{sR}, I_{lR} KaR,KdR,KsR,IlR等
-
OpenGL speciffication中的公式本质是Blinn模型公式内容懒得敲
-
设置光照参数
- 设置好物体的法向
I
=
K
a
∗
I
a
+
K
d
∗
I
l
∗
(
N
⋅
L
)
+
K
s
∗
I
l
∗
(
N
⋅
H
)
n
I = K_a*I_a + K_d*I_l*(N\cdot L) + K_s*I_l*(N\cdot H)^n
I=Ka∗Ia+Kd∗Il∗(N⋅L)+Ks∗Il∗(N⋅H)n
glNormal3f(Nx, Ny, Nz)
- 片面的法向:两个向量的叉称就是垂直于他们的方向
- 点的法向:点周围面片的法向相加平均得到的法向就是点的法向
- 打开光照
glEnable(GL_LIGHTING);
总的灯的开关glEnable(GL_LIGHT0);
OpenGL提供多个灯从0~7
- 设置光照参数
glLightfv(GL_LIGHT0, GL_AMBIENT, vLitAmbient);
设置 I a I_a IaglLightfv(GL_LIGHT0, GL_DIFFUSE, vLitDiffuse);
设置 I l I_l IlglLightfv(GL_LIGHT0, GL_SPECULAR, vLitSpecular);
设置 I l I_l IlglLightfv(GL_LIGHT0, GL_POSITION, vLitPosition);
设置光源的位置
- 设置材质
glMaterialfv(GL_FRONT, GL_AMBIENT, vMatAmb)
设置前向面的环境光反射系数 K a K_a KaglMaterialfv(GL_FRONT, GL_DIFFUSE, vDiffuse)
设置前向面的漫反射系数 K d K_d KdglMaterialfv(GL_FRONT, GL_SPECULAR, vMatSpe)
设置前向面的高光反射系数 K s K_s KsglMaterialfv(GL_FRONT, GL_SHININESS, vShininess)
设置前向面的高光指数 n n nglMaterialfv(GL_FRONT, GL_EMISSION, vEmission)
设置前向面的自发光- 第一个参数除了
GL_FRONT
还有GL_BACK
,GL_FRONT_AND_BACK
- 其他效果
- 激光灯
glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, vSpotDir);
聚光灯的方向glLightfv(GL_LIGHT0, GL_SPOT_CUTOFF, vLitCutoff);
聚光灯的角度(聚光灯的光像一个扇形)glLightfv(GL_LIGHT0, GL_SPOR_EXPONENT, vSpotExp);
聚光灯的偏离角度的衰减(偏离角度越多 光线越差)
- 光的衰减
glLightfv(GL_LIGHT0, GL_CONSTANT_ATTENUATION, kc);
常数衰减,默认为1glLightfv(GL_LIGHT0, GL_LINEAR_ATTENUATION, kl);
线性衰减,默认为0glLightfv(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, kq);
二次方衰减,默认为0- 衰 减 因 子 = 1 k c + k l ∗ d + k q ∗ d 2 衰减因子 = \frac{1}{k_c+k_l*d+k_q*d^2} 衰减因子=kc+kl∗d+kq∗d21 d就是光到点的距离,衰减因子越小光纤约暗
- 激光灯
- 设置好物体的法向
I
=
K
a
∗
I
a
+
K
d
∗
I
l
∗
(
N
⋅
L
)
+
K
s
∗
I
l
∗
(
N
⋅
H
)
n
I = K_a*I_a + K_d*I_l*(N\cdot L) + K_s*I_l*(N\cdot H)^n
I=Ka∗Ia+Kd∗Il∗(N⋅L)+Ks∗Il∗(N⋅H)n
GLSL着色器
代码片段
// 声明GLSL的版本为3.3
#version 330 core
// 传入参数
in vec4 vPosition;
in vec4 vColor;
// 传出数据
out vec4 color;
// 程序控制输入数据
uniform mat4 ModelViewProjectionMaxtrix;
void
main()
{
color = vColor; // 颜色不变
gl_Position = ModelViewProjectionMaxtrix * vPosition; // 位置根据传入矩阵进行修改
// 无需返回值
}
/*
多行注释写法
*/
基础数据类型
基本数据类型
类型 | 描述 |
---|---|
float | 32位浮点值 |
double | 64位浮点值 |
int | 有符号二进制补码的32位整数 |
uint | 无符号32位整数 |
bool | 布尔值 |
- 所有变量必须在声明的同时进行初始化
int i, num = 1500;
float force, g= -9.5;
bool falling = true;
double pi = 3.1415926535;
-
作用域
- 几乎跟C语言一样
- 在任何函数之外定义的是全局变量
- 在一组大括号之内定义的,只在打括号内有效
- for(int i=0; i<cout; i++) i只在这个for循环中有效
-
隐式转换
- GLSL的隐式转换比C语言少,int f = flase 会报错
- GLSL更注重类型安全
目标类型 | 可以隐式转换到目标类型 |
---|---|
uint | int |
float | int, uint |
double | int, uint, float |
聚合类型
矩阵
基本类型 | 2D向量 | 3D向量 | 4D向量 | 矩阵类型 |
---|---|---|---|---|
float | vec2 | vec3 | vec4 | mat2 mat3 mat4 mat2x2 mat2x3 mat2x4 mat3x2 mat3x3 mat3x4 mat4x2 mat4x3 mat4x4 |
double | dvec2 | dvec3 | dvec4 | dmat2 dmat3 dmat4 dmat2x2 dmat2x3 dmat2x4 dmat3x2 dmat3x3 dmat3x4 dmat4x2 dmat4x3 dmat4x4 |
int | ivec2 | ivec3 | ivec4 | —————— |
uint | uvec2 | uvec3 | uvec4 | —————— |
bool | bvec2 | bvec3 | bvec4 | —————— |
- 构建聚合类型
vec3 vel = vec3(0.0, 2.0, 3.0);
ivec3 step = ivec3(vel);
vec4 color;
vec3 RGB = vec3(color); // 只包含color的前四个分量
vec3 whtie = vec3(1.0f); // white = (1.0, 1.0, 1.0)
vec4 trans = vec4(whtie, 0.5); // trans = (1.0, 1.0, 1.0, 0.5)
mat3 m = mat3(4.0); // 3x3的矩阵中的主对角线值为4.0,其他位置值为0
mat3 m = mat3(1.0, 2.0, 3.0,
4.0, 5.0, 6.0,
7.0, 8.0, 9.0);
vec3 row1 = vec3(1.0, 2.0, 3.0);
vec3 row2 = vec3(4.0, 5.0, 6.0);
vec3 row3 = vec3(7.0, 8.0, 9.0);
mat3 m = mat3(row1, row2, row3);
- 访问聚合类型
分量访问符 | 符号描述 |
---|---|
(x,y,z,w) | 与位置相关的分量 |
(r,g,b,a) | 与颜色相关的分量 |
(s,t,p,d) | 与纹理坐标相关的分量 |
vec3 lum = color.rrr;
color = color.abgr; // 反转color的每个分量
// 一条语句中,只能使用一种类型的访问符
vec4 color = otherColor.rgz; // 错误 z不是(rgba)访问符中的
// 访问元素不要超过变量类型范围
vec2 pos;
float oPos = pos.z; // 错误 z超过2D向量(x,y)
结构体
struct Particle{
float lifeTime;
vec3 position;
vec3 vecl;
};
Particle p = Particle(1.0, vec3(1.0, 2.0, 3.0), vec3(1.0, 2.0, 3.0));
数组
- GLSL 4.3 之后可以数组套数组,之前不可
- 使用 [ ] 来进行索引
- 不允许使用负数索引或者超出范围的索引
- 数组具有构造函数
float coff[3]; // 3个float元素的数组
float[3] coff; // 同上
int intdic[]; // 未定义维数,可以后面重新声明
float coff[3] = float[3](2.22, 3.14, 4.2); // 通过数组的构造函数创建数组
int len = coff.length(); // 获得数组长度
mat3x4 m;
m.length(); // 列数为3
m[0].length(); // 行数为4
float digitl[m.length()]; // 设置数组大小与矩阵列数相同
float digitl[gl_in.length()]; // 设置数组大小与几何着色器的输入定点数相同
存储限制符
类型修饰符 | 描述 |
---|---|
const | 将一个变量定义为只读形式。如果它初始化时使用的是一个编译时的常量,那么它本身会称为编译时的常量 |
in | 设置这个变量为着色器阶段的输入变量 |
out | 设置这个变量为着色器阶段的输出变量 |
uniform | 设置这个变量为用户应用程序传递给着色器的数据,它对于给定的图元而言是一个常量 |
buffer | 设置应用程序共享的一块可读写的内存。这块内存也作为着色器中的存储缓存(storage buffer)使用 |
shared | 设置变量是本地工作组(local work group)中共享的。它之恩那个用于计算着色器中 |
- const 存储限制符
- in 存储限制符:可以是顶点属性(对于顶点着色器),或者是前一个着色器阶段的输出变量
- out 存储限制符:定义着色器阶段的输出变量,例如着色器中输出变换后的其次坐标,或者片元着色器中输出的最终片元颜色
- buffer 存储限制符:需要在应用程序中共享一大块缓存给着色器时使用buffer,着色器对缓存可以读和写
- shared 存储限制符:只能用于计算着色器中,可以建立本地工作组共享的内存
- uniform 存储限制符:是应用程序设置好的,在图元处理中不会发生变化,所有可用的着色阶段共享,必须定义为全局变量
uniform使用
应用程序在着色器运行之前可以,可以设置着色器中uniform变量的值,且该变量在着色器计算中不可更改。
uniform vec4 BaseColor; // 着色器中定义
着色器中可以直接通过BaseColor变量名来使用,但是在应用程序中不能直接通过变量名来设置它的值。
GLSL编译器会在链接着色器程序时创建一个uniform列表,在OpenGL中通过GLint glGetUniformLocation(GLuint program, const char* name)
函数来获得uniform列表
GLint glGetUniformLocation(GLuint program, const char* name)
// name为着色器中uniform变量名,name是以NULL结尾的字符串,不存在空格
// 如果name与启用的着色器中的素有uniform变量民都不想符或者与内部保留的着色器变量名相同返回-1
void glUniform****(GLint location, ...) // 以glUniform开头的函数 来设置unifrom的值
- 样例代码
GLint timeLoc; // 着色器中uniform变量time的索引
GLfloat timeValue; // 程序运行时间
ShaderInfo shaders[] = {
{GL_VERTEX_SHADER, "triangles.vert"}, // 顶点着色器
{GL_FRAGMENT_SHADER, "triangles.frag"}, // 片段着色器
{GL_NONE, NULL}
};
GLuint program = LoadShaders(shaders);
glUseProgram(program);
timeLoc = glGetUniformLocation(program, "time");
glUniformlf(timeLoc, timeValue);
语句
算数运算符
优先级 | 操作符 | 可用类型 | 描述 |
---|---|---|---|
1 | ( ) | —— | 成组操作 |
2 | [ ] f() .(句点) ++ – | 数组、矩阵、向量 函数 结构体 算数类型 | 数组的下标 函数调用与构造函数 访问结构体的域变量或者方法 后置递增/递减 |
3 | ++ – + - ~ ~ | 算数类型 算数类型 整型 布尔型 | 前置递增/缔结 一元正/负 一元按位 非 一元逻辑非 |
4 | * / % | 算术类型 | 乘法运算 |
5 | + - | 算数类型 | 相加运算 |
6 | << >> | 整型 | 按位操作 |
7 | <> <= >= | 算数操作 | 关系比较操作 |
8 | == != | 任意 | 相等操作 |
9 | & | 整型 | 按位与 |
10 | ^ | 整型 | 按位异或 |
11 | | | 整型 | 按位或 |
12 | && | 布尔型 | 逻辑与 |
13 | ^^ | 布尔型 | 逻辑异或 |
14 | || | 布尔型 | 逻辑或 |
15 | a?b:c | 布尔型?任意:任意 | 三元操作符 |
16 | = += -= *= /= %= <<= >>= &= |= ^= | 任意 算数类型 算数类型 整型 整型 | 赋值 算数赋值 |
17 | ,(逗号) | 任意 | 操作符序列 |
操作符重载
- GLSL中大部分操作符都是经过重载的
- 如果需要进行向量和矩阵之间的乘法(操作数和顺序非常重要,必须符合规则)
- 基本限制条件是要求矩阵和向量的维度必须是匹配的
- 两个向量相乘得到的是一个逐分量相乘的新向量(即按位相乘,而不会生成矩阵)
- 两个矩阵相乘得到的是通常矩阵相乘的结果(按照矩阵乘法相乘)
流控制
- if-else
- switch
if(truth){
} else{
}
switch(int_value){
case b:
break;
case d:
break;
default:
break;
}
循环
- for
- while
- do…while
for(int i=0; i<10; i++>){
}
while(n<10){
}
do{
}while(n < 10);
流控制语句
- break
- continue
- return
- discard:丢弃当前的片元,终止着色器的执行。discard语句只能用于片元着色器中。
函数
returnType functionName([accessModifier] type1 variable1, [accessModifier] type1 variable12, ...)
{
// 函数体
return returnValue;
}
参数限制符
访问修饰符 | 描述 |
---|---|
in | 将数据拷贝到函数中(如果没有指定修饰符,默认该形式) |
const in | 将只读数据拷贝到函数中 |
out | 从函数中获取数值(因此输入函数的值是未定义的) |
inout | 将数据拷贝到函数中,并且返回函数中修改的数据 |
- 在函数中写入一个in类型的变量,相当于对变量的局部拷贝进行了修改,只在函数自身范围内产生作用
计算的不确定性
- GLSL无法保证在不同的着色器中,两个完全相同的计算会得到完全一样的结果。这一问题与CPU对应用程序进行计算时的问题相同,即不同的优化方式可能会导致结果非常细微的差异。这细微的差异对于多通道的算法会产生问题,因为各个着色器阶段可能需要计算得到完全一致的结果。
- GLSL保证着色器之间的计算不变性:invariant 或 precise关键字
- 这两个关键字都需要在图形设备上完成计算过程,来确保同一表达式的结果可以保证重复(不变)性
- 但是对于宿主计算机和图形硬件各自的计算,这两种方法都无法保证结果的完全一致性
uniform float ten; // 假设应用程序设置该值为10.0
const float f = sin(10.0); // 宿主机的编译器负责计算
float g = sin(ten); // 图形硬件负责计算
void main(){
if(f == g); // 无法保证该值相等
}
invariant
invariant gl_Position; // 内置的输出变量
invariant centroid out vec3 Color; // 自定义输出变量
invariant 可以设置任何着色器的输出变量。它可以确保如果两个着色器的输出变量使用了同样的表达式,并且表达式中的变量也是相同值,那么产生的结果也是相同的。
输出变量的作用是将一个着色器的数据从一个阶段传递到下一个。可以在着色器中用到某个变量或者内置变量之前的任何位置,对该变量设置关键字invariant
// 如果需要将着色器中的所有可变量都设置为invariant
#param STDGL invariant(all)
precise
precise可以设置任何计算中的变量或者函数的返回值。它的作用不是增加数据精度而是增加计算的可复用性。
这段还没弄懂 后面写代码注意一下
数据块接口
着色器和应用程序之间,着色器各个阶段之间共享的变量可以组织为变量快的形式,并且有时候必须采用这种形式。uniform变量可以使用uniform块,输入和输出变量可以使用in和out块,着色器的存储缓存可以使用buffer块
// uniform块的写法
uniform b{ // 限定访问符可以是uniform in out 或者 buffer
vec4 v1; // 块中的变量列表
bool v2;
}; // 访问块成员时使用v1,v2
uniform b{
vec4 v1;
bool v2;
}name; // 访问块成员时使用name.v1 name.v2
上述代码中,块开始的部分(代码中为 b)对应于外部访问时的接口名称,结尾部分的名称(代码中为name)用于在着色器代码中访问具体成员变量
如果着色器程序变得复杂,那么用到unifrom变量的数量会上升,通常多个着色器程序会用到同一个uniform变量。
由于uniform变量的索引位置是在glLinkProgram()的时候产生的,所以应用程序获得的索引有可能发生变化。
而uniform缓存对象就是优化对uniform变量的访问,以及在不同着色器程序之间共享uniform数据的方法
着色器编译
- 为什么创建多个着色器对象
- 可能在不同的程序中复用同一个GLSL程序
- 调用
glCreateShader
来创建着色器对象 - 将着色器的源代码关联到这个对象上,使用
glShaderSource
- 如果要编译着色器对象的源代码,使用
glCompileShader
- 如果编译错误,可以通过调取编译日志来判断错误的原因
glGetShaderInfoLog
函数会返回一个具体实现相关问题的信息
- 当创建并编译了所有必要的着色器对象之后,创建可执行程序,
glCreateProgram
(创建空的着色器程序) - 将空的着色器程序关联到必要的着色器对象上
glAttachShader
- 如果要从程序中移除一个着色器对象
glDetachShader
- 当所有必要的着色器惯量到着色器程序之后,可以链接对象来生成可执行程序
glLinkProgram
- 由于着色器对象中可能存在问题,因此链接过程可能失败
- 调用
glGetProgramiv
来查询连接操作的结果,如果返回GL_TRUE则链接成功,GL_FALSE链接失败 - 如果链接失败,调用
glGetProgramInfoLog
来获取程序链接的日志信息
- 如果链接成功,可以使用
glUseProgram
来启用顶点或者片元程序 - 当着色器任务完成之后,可以通过
glDeleteShader
来删除 - 当不再需要着色器程序之后调用
glDeleteProgram
来删除着色器程序 - 最后,调用
glIsShader
来判断某个着色器对象是否存在,或者调用glIsProgram
来判断着色器程序是否存在
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <math.h>
#include <cstring>
int main(int argc, char* argv[])
{
glfwInit(); // 初始化glfw
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // OpenGL版本为3.3 主版本号为设置为3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // OpenGL版本为3.3 主版本号设置为3
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心模式 无需后向兼容性
//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // MacOs 加上
glfwWindowHint(GLFW_RESIZABLE, true); // 不可更改窗口大小
// 设置宽高
int screen_width = 1280;
int screen_height = 720;
// 设置窗口
auto window = glfwCreateWindow(screen_width, screen_height, "Computer", nullptr, nullptr);
// 如果窗口创建失败 输出Failed to create OpenGL Context
if (window == nullptr)
{
std::cout << "Failed to create OpenGL Context" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window); // 将窗口的上下文设置为当前线程的主上下文
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) // 初始化GLAD
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
glViewport(0, 0, screen_width, screen_height); // 创建视口
// 上面生成创建 窗口 下面处理数据 设置属性指针
// 三角形的顶点数据 数据需要在-1~1之间
const float triangle[] = {
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f, // 右下
0.0f, 0.5f, 0.0f // 正上
};
// 生成和绑定VBO和VAO
GLuint vertext_array_object; // VAO 可以一次性发送多个数据 而不用一个个发送
glGenVertexArrays(1, &vertext_array_object);// 生成
glBindVertexArray(vertext_array_object); // 绑定
GLuint vertext_buffer_object; // VBO
glGenBuffers(1, &vertext_buffer_object); // 生成缓冲区
glBindBuffer(GL_ARRAY_BUFFER, vertext_buffer_object); // 绑定
// 将顶点数据绑定到默认的缓冲中
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);
// 设置顶点属性指针 告诉GPU如何解释顶点数据
// 0 顶点着色器的位置值(这里没有);3 顶点属性是三分量的向量 ;GL_FLOAT 顶点类型
// GL_FALSE 表示是否希望数据标准化即映射到0~1 ;3 * sizeof(float) 步长,连续顶点属性之间的间隔,这里表示下一个顶点的数据在3个float之后 ;(void*) 0 数据的偏移量,因为给定数组一开始就是数据,所以偏移量为0,注意需要强转类型
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 开启通道 默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,意味着数据在着色器端是不可见的
// 设置属性指针完成后 需要解绑VBO和VAO
// 1. 防止之后绑定的VAO VBO影响当前的VAO VBO
// 2. 使代码灵活规范 在需要渲染的时候绑VAO 然后绘制
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 使用顶点和片段着色器 着色器源码 -> 生成并编译着色器 -> 链接着色器到着色器程序 -> 删除着色器
const char* vertext_shader_source = // 顶点着色器这里其实什么都没做
"uniform float time;\n"
"uniform vec4 Fragcol;\n"
"in vec4 vertCol;\n"
"out vec4 col;"
"void main()\n"
"{\n"
" vec4 v = gl_Vertex;\n"
" v.y += time*0.1;\n"
" col = Fragcol;\n"
" gl_Position = gl_ModelViewProjectionMatrix * v;\n"
"}\n\0";
const char* fragment_shader_source = // 片段着色器 设置颜色rgba = (1.0f, 0.5f, 0.2f, 1.0f)
"in vec4 col;"
"void main()\n"
"{\n"
" gl_FragColor = col;\n"
"}\n\0";
// 编译着色器
// 顶点着色器
int vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader, 1, &vertext_shader_source, NULL);
glCompileShader(vertex_shader);
int success = -1;
char info_log[512];
// 检查着色器是否成功过编译,如果失败打印错误信息
glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
std::cout << "Error::Shader::Vertext::Compilation_failed\n" << info_log << std::endl;
}
// 片段着色器
int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
glCompileShader(fragment_shader);
memset(info_log, 0, sizeof(info_log));
// 检查着色器是否成功过编译,如果失败打印错误信息
glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
std::cout << "Error::Shader::Fragnebt::Compilation_failed\n" << info_log << std::endl;
}
// 链接顶点和片段着色器至一个着色器程序中
int shader_program = glCreateProgram();
glAttachShader(shader_program, vertex_shader); //添加顶点着色器
glAttachShader(shader_program, fragment_shader); //添加片段着色器
glLinkProgram(shader_program);
// 检查链接是否成功 不成功打印错误信息
glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
if (!success)
{
memset(info_log, 0, sizeof(info_log));
glGetProgramInfoLog(shader_program, 512, NULL, info_log);
std::cout << "Error::Shader::Program::Compilation_failed\n" << info_log << std::endl;
}
// 然后就可以删掉着色器了 只需要使用编译好的program就可以了 着色器就不需要了
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);
// 开启线框模式 可以解开注释看看效果
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
while (!glfwWindowShouldClose(window))
{
static float shaderTime = 0.0f;
static float moveOrnament = 0.01f;
shaderTime += moveOrnament;
if (shaderTime > 5.0f)
{
moveOrnament = -0.01f;
}
else if (shaderTime < -5.0f)
{
moveOrnament = 0.01f;
}
// 清空颜色缓冲
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // 使用黑色填充(最顶层的背景颜色)
glClear(GL_COLOR_BUFFER_BIT);
// 使用着色器程序
glUseProgram(shader_program);
GLint time_location = glGetUniformLocation(shader_program, "time"); // 获得time在shader中索引
glUniform1f(time_location, shaderTime);
GLint col_location = glGetUniformLocation(shader_program, "Fragcol"); // 设置方框的颜色
glUniform4f(col_location, 122.0f / 255.0f, 122.0f / 255.0f, 122.0f / 255.0f, 1.0f);
// 绘制三角形
glBindVertexArray(vertext_array_object); // 绑定VAO
glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形
glBindVertexArray(0); // 解除绑定
// 交换缓冲并检查是否有触发事件(键盘鼠标等)
glfwSwapBuffers(window);
glfwPollEvents();
}
// 删除VAO和VBO
glDeleteVertexArrays(1, &vertext_array_object);
glDeleteBuffers(1, &vertext_buffer_object);
// 清理所有的资源并正确退出程序
glfwTerminate();
return 0;
}
通过设置着色器让物体动起来
OpenGL绘制方式
图元
- OpenGL支持很多不同的图元类型
- 点,线,或者三角形(可以再组合为条带、循环体、扇面等,大多数设备都支持的基础图元类型)
- 作为细分器输入的Patch类型
- 作为几何着色器输入的临街图元
点
-
通过单一的顶点来表示,一个点就是一个四维的其次坐标值,因此点不存在面积
-
OpenGl通过显示屏幕上的一个四边形区域来模拟点
-
渲染点图元的时候,OpenGL会通过一系列光栅化规则来判断点所覆盖的像素位置
-
设置点的大小
- 默认的点大小为1.0
glPointSize(GLfloat size)
- 可以在顶点、细分、几何着色器中设置gl_PointSize来设置
线、条带与循环
-
OpenGL中的线指的是线段,独立的线用一对顶点来表达,每个顶点为线段端点
- 多线段链接首位闭合的叫做 循环线
- 开放的多线段叫做 条带线
-
设置线段的宽度
void glLineWidth(GLfloat width)
- width表示线的宽度,必须是一个大于0.0的值
三角形、条带和扇面
- 三角形:独立的三角形
- 条带:相邻三角形共享一条边
- 扇面:第一个点作为共享点存在
两个三角形的共享边上的像素值因为同时被两者所覆盖,因此不可能不收到光照计算的影响
两个三角形的共享边上的像素值,不可能受到多于一个三角形的光照计算影响
上面两句话总结就是:OpenGL对于模型三角形共享边的光栅化过程不会产生任何裂缝,也不会产生重复的绘制
OpenGL图元的模式标识
图元模型 | OpenGL枚举量 |
---|---|
点 | GL_POINTS |
线 | GL_LINES |
条带线 | GL_LINESTRIP |
循环线 | GL_LINE_LOOP |
独立三角形 | GL_TRIANGLES |
三角形条带 | GL_TRIANGLE_STRIP |
三角形扇面 | GL_TRIANGLE_FAN |
将多变形渲染为点集、轮廓线、实体
- 一个多变形有 正面和背面,不同的面朝向观察者时可能会有不同的渲染结果
- 默认情况下正面和背面的绘制方法是一样的,如果要修改这个属性需要设置
glPolygonMode
void glPolygonMode(GLenum face, Glenum mode);
// face 必须是 GL_FRONT_ANDBACK
// mode 可以是 GL_POINT、GL_LINE、GL_FILL
// 绘制模式是 点集、轮廓线、填充模式
多变形的反转和裁剪
-
一般来说,多变形的顶点在屏幕上应该是逆时针方向排列的(法线向上,右手比划一下大拇指向上时其余手指弯曲方向是逆时针)
- 对于球体、环形体、茶壶等都是有向的
- 对于莫比乌斯环、克林瓶不是逆时针
-
如果我们要描述一个有向的模型表面,但是它的外侧需要使用逆时针方向进行描述,使用
glFrontFace
来反转
void glFrontFace(GLenum mode);
// 控制多变形正面的判断方式
// 默认模式是GL_CCW:多变形投影到窗口坐标系之后,顶点按照逆时针排列的面作为正面
// 模式是GL_CW: 采取顺时针方向的面将被认为是正面
- 对于一个由不透明的且方向一致的多变形组成、完全封闭的模型表面来看说,它的所有背面多变形都是不可见的——会背正面多边形遮挡
- 如果位于模型外侧,开启裁剪可来直接抛弃OpenGL中的背面多变形
- 如果位于模型内存,需要指示OpenGL自动抛弃正面或者背面的多变形
void glCullFace(Glenum mode);
// 在转换到屏幕空间渲染之前,设置需要抛弃(裁剪)哪一类多变形
// mode 可以是:GL_FRONT、GL_BACK或者GL_FRONT_AND_BACK
// 分别表示:正面、背面或者所有多变形
void glEnable()
// GL_CULL_FACE参数来开启裁剪
void glDisable()
// 同样的 GL_CULL_FACE来关闭
OpenGL缓存数据
- 几乎所有使用OpenGL完成的事情都用到的了缓存buffer中的数据
- OpenGL的缓存表示为缓存对象
创建与分配内存
void glGenBuffers(GLsize n, GLuint *buffers);
// 返回n个当前未使用的缓存对象名称,并保存到buffers数组中
buffers得到一个缓存对象名称的而数组,需要将名称绑定到系统环境中的一个结合点,这样缓存对象才会真正创建出来
目标 | 用途 |
---|---|
GL_ARRAY_BUFFER | 这个结合点可以用来保存glVertexAttribPointer()设置的顶点数组数据 |
CL_COPY_READ_BUFFER 和 GL_COPY_WRITE_BUFFER | 这两个目标是一堆互相匹配的结合点,用于拷贝缓存之间的数据,并且不会引起OpenGL的状态变化,也不会产生任何特殊形式的OpenGL调用 |
GL_DRAW_INDIRECT_BUFFER | 如果采用间接绘制的方法,那么这个缓存目标用于存储绘制命令的参数 |
GL_ELEMENT_ARRAY_BUFFER | 绑定到这个目标的缓存中可以包含顶点索引数据,以便用于glDrawElements()等索引形式的绘制命令 |
GL_PIXEL_PACK_BUFFER | 这一缓存目标用于从图像中读取数据了,相关OpenGL命令包括 glGetTexImage 和 glReadPixels |
GL_TEXTURE_BUFFER | 与GL_PIXEL_PACK_BUFFER相反,它可以作为glTexImage2D等命令的数据源使用 |
GL_TRANSFORM_FEEDBACK_BUFFER | transform feedback 是OpenGL提供的一种便捷方案,它可以在管线的顶点处理部分结束时(即经过顶点着色,可能还有集合着色阶段),将经过变换的顶点重新捕获,并且将部分属性写入到缓存对象中。 |
GL_UNIFORM_BUFFER | 用于创建uniform缓存对象的缓存数据 |
void glBindBuffer(GLenum target, GLuint buffer);
// 将名称为buffer的缓存对象绑定到target所指定的缓存结合点
// target必须是OpenGL支持的缓存帮绑定目标直以,buffer必须是通过glGenBuffers分配的缓冲对象
// 如果buffer是第一次被绑定,那么所对应的缓存对象也将被同时创建
- 样例代码
GLuint vbo[3];
glGenBuffers(3,vbo);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
缓存输入和输出数据
void glBufferData(GLenum target, GLsizeiptr size, GLvoid* data, GLenum usage);
// 为绑定到target的缓存对象分配size大小(单位为字节)的存储空间
// 如果参数data不是NULL,那么将使用data所在的内存区域的内容来初始化整个空间
// usage允许应用程序向OpenGL端发送一个提示,指示缓存中的数据可能具备一定特定的用途
- usage必须是内置标准标识符中的一个
- GL_STATIC_DRAW
- GL_DYNAMIC_COPY
- 等等…… 下面有分解标识符的含义
分解的usage标识符 | 意义 |
---|---|
GL_STATIC_ | 数据集存储内容只写入一次,然后多次使用 |
GL_DYNAMIC_ | 数据存储内容会背反复写入和反复使用 |
GL_STREAM_ | 数据存储内容只写入一次,然后也不会被频繁使用 |
_DRAW | 数据存储内容有应用程序负责写入,并且作为OpenGL绘制和图像命令的数据源 |
_READ | 数据存储内容通过OpenGL反馈的数据写入,然后再应用程序进行查询时返回这些数据 |
_COPY | 数据存储内容通过OpenGL反馈的数据写入,并且作为OpenGL绘制和图像命令的数据源 |
- 缓存的部分初始化
void glBufferSubData(Glenum target, GLintptr offset, GLsizeiptr size, const GLvoid* data);
// 使用新的数据替换缓存对象中的部分数据
// 绑定到target的缓存对象要从offset字节处开始需要使用地址data、大小为size的数据块来进行更新
// 如果offset和size的综合超出缓存对象绑定数据的范围,那么将产生一个错误
- 样例代码
// 顶点位置
static const GLfloat position[] = {
-1.0f, -1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 1.0f
};
// 顶点颜色
static const GLfloat colors[] = {
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f,
1.0f, 1.0f, 1.0f,
};
// 缓存对象
GLuint buffer;
// 为缓存对象生成一个名称
glGenBuffer(1, &buffer);
// 将它绑定到GL_ARRAY_BUFFER目标
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// 分配足够的空间(sizeof(position) + sizeof(colors))
glBufferData(GL_ARRAY_BUFFER, // 目标
sizeof(position) + sizeof(colors), // 总计大小
NULL, // 无数据
GL_STATIC_DRAW); // 用途
glBufferSubData(GL_ARRAY_BUFFER, // 目标
0, // 偏移地址
sizeof(positions), // 大小
positions); // 数据
glBufferSubData(GL_ARRAY_BUFFER, // 目标
sizeof(positions), // 偏移地址
sizeof(colors), // 大小
colors); // 数据
- 将缓存对象的数据清楚为一个已知的值
glClearBufferData
glClearBufferSubData
void glClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data);
void glClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLintptr size, GLenum format, GLenum type, const void* data);
// 清楚缓存对象中的所有或者部分数据
// 绑定到target的缓存存储空间将使用data中存储的数据进行填充。format和type分别制定了data对象数据的格式和类型
// 首先将数据转换到internalformat所指定的格式,然后填充缓存数据的指定区域范围
对于glClearBufferData()来说,整个区域都会被指定的数据所填充
对于ClearBufferSubData()来说,填充区域时通过offset和size来指定,分别指定以字节为单位的起始偏移地址和大小
- 缓存对象中的数据也可以使用
glCopyBufferSubData()
函数互相进行拷贝- 我们可以使用
glBufferData
将数据更新到独立的缓存中,然后将这些缓存直接用glCopyBufferSubData
拷贝到一个较大的缓存中
- 我们可以使用
void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset, GLintprr writeoffset, GLsizeiptr size);
// 将绑定到readtarget的缓存对象的一部分存储数据拷贝到与writetarget相绑定的缓存对象的数据区域中
// readtarget对应的数据从readoffset位置开始赋值size个字节,然后拷贝到writetarget对应数据的writeoffset位置
// 如果readoffset或者writeoffset与szie的和超出了绑定的缓存对象的范围,那么OpenGL产生GL_INVALID_VALUE错误
- 读取缓存的内容:
glGetBufferSubData()
可以绑定到某个目标的缓存中回读数据,然后将它放置到应用程序保有的一处内存当中
void glGetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, GLvoid* data);
// 返回当前绑定到target的缓存对象中的部分或者全部数据
// 起始数据的偏移字节位置为offset,回读的数据大小为size个字节,他们将从缓存的数据区域拷贝到data所指向的内存区域中
// 如果缓存对象当前已经被映射,或者offset和size的和超出缓存对象数据区域的范围,那么将提示错误
- 访问缓存的内容
- 上述的
glBufferData
、glBufferSubData
、glCopyBufferSubData
和glGetBufferSubData
都会进行一次数据拷贝 glBufferData
、glBufferSubData
将数据从应用程序内存拷贝到OpenGL内存中,glCopyBufferSubData
OpenGL内部拷贝到内部,glGetBufferSubData
OpenGL内存拷贝到应用程序中glMapBuffer
可以直接在应用程序中对OpenGL管理的内存进行访问
- 上述的
void* glMapBuffer(GLenum target, Glenum access);
// 将当前绑定到target的缓存对象的整个数据区域映射到客户端的地址空间中
// 根据给定的access策略,通过返回的指针对数据进行直接读或者写的操作
// 如果OpenGL无法将缓存对象的数据映射出来,那么glMapBuffer将产生一个错误并且返回NULL
标识符 | 意义 |
---|---|
GL_READ_ONLY | 应用程序仅对OpenGL映射的内存区域执行读操作 |
GL_WRITE_ONLY | 应用程序仅对OpenGL映射的内存区域执行写操作 |
GL_READ_WRITE | 应用程序对OpenGL映射的内存区域可能执行读或者写操作 |
access相当于程序与用户的约定,如果违反约定可能出现操作被忽略、数据被破坏、程序崩溃
- 如果使用
glMapBuffer
获得指针,操作了数据之后,需要使用glUnmapBuffer
来解除映射操作
GLboolean glUnmapBuffer(GLenum target);
// 解除glMapBuffer创建的映射
// 如果对象数据没有损坏,返回GL_TRUE
// 如果对象数据损坏,返回GL_FALSE
可以执行一个如下操作:
可以先使用glBufferData
分配空间,之后进行映射glMapBuffer
,直接写入数据,最后解除映射
GLuint buffer;
FILE* f;
size_t filesize;
// 打开文件确定大小
f = fopen("data.dat", "rb");
fseek(f, 0, SEEK_END);
filesize = ftell(f);
fseek(f, 0, SEEK_SET);
// 生成缓存名字并将它绑定到缓存绑定点上
// 创建缓存
glGenBuffers(1, &buffer);
glBindBuffer(GL_COPY_WRITE_BUFFER, buffer);
// 分配缓存中存储的数据空间,向data参数传入NULL即可
glBufferData(GL_COPY_WRITE_BUFFER, (GLsizei)filesize, NULL, GL_STATIC_DRAW);
// 映射缓存
void* data = glMapBUffer(GL_COPY_WRITE_BUFFER, GL_WRITE_ONLY);
// 将文件读入缓存
fread(data, 1, filesize, f);
// 关闭文件 解除映射
glUnmapBuffer(GL_COPY_WRITE_BUFFER);
fclose(f);
顶点规范
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* pointer);
// 设置顶点属性在index位置可访问的数据值,pointer的起始位置也就是数组中的第一组数据值,由绑定到GL_ARRAY_BUFFER目标的缓存对象中的地址偏移量确定的
// size表示每个定点中需要更新的元素个数,type表示每个元素的数据类型,normalized表示顶点数据是否需要在传递到顶点数组之前进行归一化处理
// stride表示数组中两个连续元素之间的偏移字节数,如果stride为0,那么内存中各个数据就是紧密贴合的
glVertexAttribPointer
所设置的状态会保存到当前绑定的数组对象(VAO)中
size表示属性向量的元素个数(1,2,3,4)或者一个特殊的标识符GL_BGRA
type设置缓存对象中存储的数据类型
type标识符 | OpenGL类型 |
---|---|
GL_BYTE | GLbyte(有符号8位整型) |
GL_UNSIGNED_BYTE | GLubyte(无符号8位整型) |
GL_SHORT | GLshort(有符号16位整型) |
GL_UNSIGNED_SHORT | GLushort(无符号16位整型) |
GL_INT | GLint(有符号32位整型) |
GL_UNSIGNED_INT | GLuint(无符号32位整型) |
GL_FIXED | GLfixed(有符号16位定点型) |
GL_FLOAT | GLfloat(32位IEEE单精度浮点型) |
GL_HALF_FLOAT | GLhalf(16位S1E5M10半精度浮点型) |
GL_DOUBLE | GLdouble(64位IEEE双精度浮点型) |
GL_INT_2_10_10_10_REV | GLuint(压缩数据类型) |
GL_UNSIGNED_INT_2_10_10_10_REV | GLuint(压缩数据类型) |
如果type为GL_INT等整型,而normalize位GL_FALSE,那么如果整数将被强制转换成浮点数的形式,然后再传入顶点着色器
void glVertexAttribIPointer(GLuint index, GLint size, GLenum type, GLsizei stride, const GLvoid* pointer);
// 与glVertexAttribPointer相似,不过它专用于向顶点着色器中传递整型的顶点属性
// type 包括 GL_BYTE、GL_UNSIGNED_BYTE、GL_SHORT、GL_UNSIGNED_SHORT、GL_INT以及GL_UNSIGNED_INT
glVertexAttribIPointer
与glVertexAttribPointer
完全等价,只是不用再传normalize参数
void glVertexAttribLPointer(GLuint index, GLint size, GLenum type, GLsizei stride, const GLvoid* pointer);
// 与glVertexAttribPointer相似,不过它专用于向顶点着色器中传递双精度浮点型的顶点属性
// 使用glVertexAttribPointer时即使指定type为GL_DOUBLE类型,也会转到单精度浮点型
void glEnableVertexAttribArray(0); // 开启通道 默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,意味着数据在着色器端是不可见的
void glDisableVertexAttribArray();
在OpenGL从顶点缓存中读取数据之前,必须使用
glEnableVertexAttribArray
启用对应的顶点属性数组
如果没启用顶点属性对应的属性数组的话,OpenGL会使用静态顶点属性(使用一个常数来填充模型中所有顶点的数据缓存)
- 设置静态顶点属性:每个属性的静态顶点属性可以通过
glVertexAttrib*()
系列函数来设置
void glVertexAttrib{1234}{fds}(GLuint index, TYPE values);
void glVertexAttrib{1234}{fds}v(GLuint intdex, const TYPE* values);
void glVertexAttrib4{bdidf ub us ui}v(GLuint index, const TYPE* values);
// 设置索引为index的顶点属性的静态值。
// 如果函数名称尾部没有v,那么最多可以指定4个参数值,即x、y、z、w参数
// 如果函数尾部由v,呢么最多有4个参数值时保存在一个数组中传入的,它的地址通过value来指定,顺序依次为x、y、z、w
这些函数会自动将传入参数转换为浮点数,然后传递到顶点着色器中
- 如果函数中需要传入整型数值,可以用其他函数,将数据归一化到[0,1]或者[-1, 1]的范围内
void glVertexAttrib4Nub(GLuint index, GLubyte x, Glubyte y, GLubyte z, GLubyte w);
void glVertexAttrib4N{bsi ub us ui}v(GLuint index, const TYPE* v);
在转换过程中将无符号参数归一化到[0,1]的范围,将有符号参数归一化到[-1, 1]的范围
- 如果顶点属性必须声明为整数或者双精度浮点数的话,可以使用下面的函数
void glVertexAttribI{1234}{i ui}(GLuint index, TYPE values):
void glVertexAttribI{123}{i ui}v(GLuint index, const TYPE* values):
void glVertexAttribI4{bsi ub us ui}v(GLuint index, const TYPE* values)
// 设置一个或多个静态整型顶点属性值
// index表示顶点着色器中指定索引的顶点属性。
// values表示静态数据。
void glVertexAttribL{1234}(GLuint index, TYPE values):
void glVertexAttribL{1234}v(GLuint index, const TYPE* values):
// 设置一个或多个静态双精度浮点顶点属性值
// index表示顶点着色器中指定索引的顶点属性。
// values表示静态数据。