OpenGL Hello World
参考学习资料:
https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
程序代码地址:
https://github.com/Hujunjob/OpenGLPro
顶点数据Vertex Data --> 顶点着色器Vertex Shader -> 几何着色器Geometry Shader --> 光栅化 --> 片元着色器Fragment Shader --> 混合和Alpha测试 Blending and Alpha Test
其中,顶点着色器和片元着色器是必须实现的,几何着色器可以不实现用默认的。
一、VBO:将顶点数据放入显存
//定义顶点数组
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
//顶点着色器会在GPU上创建内存,用于存储顶点数据
//需要配置OpenGL如何解释这些内存,指定如何发送给显卡
//通过顶点缓冲对象VBO来管理这个内存,在GPU内存(显存)中存储大量顶点
//这是从CPU发送到GPU,比较慢,所以尽量一次发送尽可能多的数据
//生成一个VBO对象,用一个id来表示
unsigned int VBO;
//着色器程序
uint mProgram;
void generateVBO() {
//生成一个VBO缓冲对象
glGenBuffers(1, &VBO);
//OpenGL有很多缓冲对象,VBO的缓冲类型是GL_ARRAY_BUFFER
//同一种类型的缓冲对象只能绑定一个id
//将VBO绑定到GL_ARRAY_BUFFER类型上
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//当VBO被绑定到GL_ARRAY_BUFFER上时,所有操作GL_ARRAY_BUFFER的配置都是配置VBO了
//接下来将顶点数据保存到VBO上,也就是保存到GL_ARRAY_BUFFER上
//将数据保存到显存里,最后一个参数是保存方式,显卡如何管理这些数据
//有三种管理方法
//GL_STATIC_DRAW :数据不会或几乎不会改变。
//GL_DYNAMIC_DRAW:数据会被改变很多。
//GL_STREAM_DRAW :数据每次绘制时都会改变。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
}
二、编译自定义着色器
首先先自定义顶点着色器和片元着色器:
顶点着色器vtriangle.vert
#version 320 es
layout (location = 0) in vec3 aPos;
void main() {
gl_Position = vec4(aPos, 1.0);
}
片元着色器ftriangle.glsl
#version 320 es
precision lowp float;
out vec4 FragColor;
void main() {
FragColor = vec4(1.0f,0.5f,0.2f,1.0f);
}
编译着色器
//动态编译顶点着色器源码
//创建着色器对象,还是用id存储
unsigned int generateVertexShader() {
unsigned int vertexShaderId = 0;
//创建shader
vertexShaderId = glCreateShader(GL_VERTEX_SHADER);
const char *vShader = readFromAsset("vtriangle.vert");
//将着色器代码附着到着色器对象上
glShaderSource(vertexShaderId, 1, &vShader, nullptr);
//编译着色器代码
glCompileShader(vertexShaderId);
//检测是否编译成功
int success;
char infoLog[512];
glGetShaderiv(vertexShaderId, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShaderId, 512, nullptr, infoLog);
LOGE("opengl", "generateVertexShader 编译顶点着色器错误 %s", infoLog);
}
return vertexShaderId;
}
//动态编译片段着色器
unsigned int generateFramgentShader() {
unsigned int fragmentShaderId = 0;
fragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER);
const char *fShader = readFromAsset("ftriangle.glsl");
glShaderSource(fragmentShaderId, 1, &fShader, nullptr);
glCompileShader(fragmentShaderId);
int success;
char infoLog[512];
glGetShaderiv(fragmentShaderId, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(fragmentShaderId, 512, nullptr, infoLog);
LOGE("opengl", "generateVertexShader 编译片元着色器错误 %s", infoLog);
}
return fragmentShaderId;
}
三、链接着色器形成着色器程序
//着色器程序是多个着色器合并后并最终链接在一起完成的版本
//链接多个着色器,会把一个着色器的输出当做另一个着色器的输入,如果输出输入不匹配,则会链接失败
unsigned int generateProgram(uint vShader, uint fShader) {
//创建 --> 附着 --> 链接
uint program = glCreateProgram();
glAttachShader(program, vShader);
glAttachShader(program, fShader);
glLinkProgram(program);
//检测
int success;
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
char info[512];
glGetProgramInfoLog(program, 512, nullptr, info);
LOGE("opengl", "链接程序错误 %s", info);
return 0;
} else {
LOGD("opengl", "链接成功");
return program;
}
}
uint linkProgram() {
auto vShader = generateVertexShader();
if (vShader == 0) {
LOGD("opengl", "linkProgram 顶点着色器生成失败");
return 0;
}
auto fShader = generateFramgentShader();
if (fShader == 0) {
LOGD("opengl", "linkProgram 片元着色器生成失败");
return 0;
}
auto program = generateProgram(vShader, fShader);
if (program == 0) {
LOGD("opengl", "linkProgram 链接失败");
return 0;
}
//激活程序对象,这样以后渲染都是调用这个着色器程序了
glUseProgram(program);
//删除
glDeleteShader(vShader);
glDeleteShader(fShader);
return program;
}
四、让OpenGL解析顶点数据
//由于顶点着色器允许指定各种形式的输入,所有我们需要让OpenGL知道如何读取这些数据
//例如我们当前的VBO是顶点数据以XYZ形式连续排列,所以顶点连续排列
void handleVBO() {
//告诉OpenGL如何读取VBO数据
//各个参数说明
//
// 第一个参数指定从索引0开始取数据,与顶点着色器中layout(location=0)对应。
//
// 第二个参数指定顶点属性大小,顶点属性是vec3,则由3个值xyz组成
//
// 第三个参数指定数据类型。
//
// 第四个参数定义是否希望数据被标准化(归一化),只表示方向不表示大小。
//
// 第五个参数是步长(Stride),指定在连续的顶点属性之间的间隔。
//
// 第六个参数表示我们的位置数据在缓冲区起始位置的偏移量。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), nullptr);
//以上面的索引值0作为参数,启动顶点属性
glEnableVertexAttribArray(0);
}
五、绘制三角形
void onDrawFrame() {
glClearColor(0.2, 0.3, 0.3, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
LOGD("OpenGL", "onDrawFrame");
glDrawArrays(GL_TRIANGLES,0,3);
}
此时就依次完成了以下步骤:
生成并绑定VBO --> 将顶点数组存储到显存 -->编译顶点着色器和片元着色器 --> 链接两个着色器生成着色器程序Program --> 告诉OpenGL如何解析顶点数组 --> 绘制三角形
优化
VAO
以上绑定了一个三角形,但是如果我们有几十几百个物体,这样绑定和配置就太麻烦了
使用顶点数组对象VAO Vertex Array Object,可以保存所有的VBO对象和配置
VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。
uint generateVAO(){
uint vao = 0;
//生成VAO
glGenVertexArrays(1,&vao);
//绑定VAO
glBindVertexArray(vao);
}
使用VAO
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
索引缓冲对象IBO
索引缓冲对象,Element Buffer Object EBO 或 Index Buffer Object IBO。
OpenGL里绘制各种形状都是由三角形组合而成,比如矩形就是由两个三角形组合而成。
矩形有4个点,绘制两个三角形需要6个点,这样就有2个点多余了。利用IBO存储绘制顺序,在只存储4个点的情况下,告诉OpenGL调用点的顺序,可以绘制两个三角形。
//矩形由2个三角形组成,需要6个点。但是有两个点重复了,浪费存储空间
float rectangleVertices[] = {
// 第一个三角形
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 // 左上角
};
使用EBO同样可以使用VAO来存储EBO,因为都是glBindBuffer()。
//以上绑定了一个三角形,但是如果我们有几十几百个物体,这样绑定和配置就太麻烦了
//使用顶点数组对象VAO Vertex Array Object,可以保存所有的VBO对象和配置
//利用VAO可以同时存储VBO和EBO
uint generateVAO() {
uint vao = 0;
//生成VAO
glGenVertexArrays(1, &vao);
//绑定VAO
glBindVertexArray(vao);
//生成VAO
generateVBO();
//告诉OpenGL如何解析VBO并启动VBO
handleVBO();
//生成绑定EBO
generateEBO();
//解绑VAO
glBindVertexArray(0);
return vao;
}