OpenGL/C++_学习笔记(三)绘制第一个图形

汇总页
上一篇: OpenGL/C++_学习笔记(二) 引入图形渲染相关概念


OpenGL/C++_学习笔记(三) 绘制第一个图形

  • 绘制图形
    • 绘制第一个三角形的代码流程
      • 回顾
      • 窗口空间/标准化设备坐标
      • 显存数据
        • 顶点缓冲对象VBO
        • 顶点数组对象VAO
      • 着色器的使用
        • 着色器程序设计
          • 顶点着色器设计
          • 片元着色器设计
        • 着色器程序的读取、编译、链接、使用
      • 第一个三角形效果图
      • 三角形绘制完整代码示例
    • 绘制第一个长方形(引入索引缓冲对象EBO)
      • 1. 直接套用微调上文绘制三角形的代码
      • 直接套用代码进行重复绘制的效果图
      • 2. 使用索引/元素(IBO/EBO)进行优化
        • EBO绘制长方形效果图
        • EBO绘制完整代码示例
        • glDrawElements(mode, count, type, indices)函数作用的一些奇怪猜想
    • 绘制多实例的优化(XXXInstanced绘制函数相关)
      • 功能介绍
      • 多图形实例绘制效果图
      • 多图形对象绘制完整代码示例(含着色器代码)
    • 一些基础绘制相关杂项
      • 线框模式(Wireframe Mode)
        • 本文绘制内容在线框模式下的绘制结果

绘制图形

绘制第一个三角形的代码流程

回顾

复习第一节的gl在代码中的初始化流程

 // !初始化glfw窗口
 glfwInit();
 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);     // 指定OnencL的主版本号(和glad版本对应)
 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);     // 指定OpenGL的子版本号(和glad版本对应)
 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 设置使用OpenCL的核心模式(可编程渲染管道的模式)
 //创建窗口
 GLFWwindow* window = glfwCreateWindow(720, 720, "LearnOpenGL", NULL, NULL); // 参数含义:窗口宽,窗口高,窗口名称,窗口附加,窗口依赖,如果新建窗口失败返回NULL指针
 if (window == NULL)            // 如果出错,则退出程序
 {
  std::cout << "Failed to create GLFW window" << std::endl; // 报错信息
  glfwTerminate();           // 终止窗口进程
  return -1;
 }
 glfwMakeContextCurrent(window); // 将窗口的上下文环境设置为当前主线程的上下文环境 
 // !初始化glad
 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))  // 如果加载失败,退出程序
 {
  std::cout << "Failed to initialize GLAD" << std::endl;  // 报错信息
  return -1;
 }
 // 注册窗口回调函数
 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

 glClearColor(1.0f, 1.0f, 0.0f, 1.0f); // 设置清屏颜色为黄色

窗口空间/标准化设备坐标

标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):
OpenGL/C++_学习笔记(三)绘制第一个图形_第1张图片

显存数据

显存不能直接被 OpenGL 应用程序在 cpu 通过内存地址进行访问, 因此衍生出各种以gl库为中间人, 借由gl库函数(api/程序接口)对进行发令控制和交付数据, 间接管理存储对象以及缓冲区的管理标识对象; 这些API和概念主要是为了解决GPU使用内存和通信效率问题.

顶点缓冲对象VBO

CPU 和 GPU 之间通信,以及 GPU 从主存里读取数据的效率都是制约系统工作效率的瓶颈,所以使用 glGenBuffers 分配 GPU 里的显存,它不能用指针表示和读取,所以用一个整数 VBO 标识来负责管理申请分配的一块显存, 作用类似 malloc 分配的地址指针, 在调用 glBindBuffer 时传入VBO编号绑定目前工作VBO, 代表在下次绑定或解绑前相关操作均在该显存上.
glBindBuffer 具体作用是设置 Opengl 全局变量,其第一个参数表示接下来将在
glVertexAttribPointer 或 glBufferData 中所要操作的 buffer 的类型(传入 GL_ARRAY_BUFFER 代表是 VBO , 之后还有纹理缓存的等其他类型的缓存),其可以规定读写 buffer 的方式,还可以只使用 buffer 的一部分,这个 buffer 没有直接在 glVertexAttribPointer 的传参中指定(即需要先调用 glBindBuffer 进行指定)则是历史遗留问题.(OpenGL :glBindBuffer参数详解).
绑定完成后再

  1. glVertexAttribPointer 负责指定内存中数据的结构和地址偏移量,告诉 gpu 该如何从VBO管理的缓存中读取顶点属性(Vertex Attribute), 注意: VBO 并不负责维护读取属性的信息, 由接下来将提及的VAO报存.

  2. 用 glBufferData 或 glSubBufferData 把数据从主存传到分配的显存上. 注意, VBO并不会因为你多次传入数据使显卡重新分配内存而改变(区别于 cpu 的 remalloc), 在我的理解里更接近内存管理的页式存储或动态存储分配给程序的页号/标识符(未考证, 涉及显卡本身的内存管理策略, 可能是由各家显卡驱动独立实现, 我没有找到开源信息).

注意: glBufferData 和 glVertexAttribPointer 并无必然的先后关系, 只是都需要在 glBindBuffer 后执行.

顶点数组对象VAO

OpenGL 是 CPU 指挥 GPU 做事,虽然已经通过 VBO 管理发送到 GPU 端的大块数据了,但要由于其只负责管理缓存, 并不知道本VBO下数据的读取方式, 若 glVertexAttribPointer 和 VBO 相同–以全局变量形式激活, 若不单独保持管理, 绘制很多东西的时候就需要反复把各个顶点属性绑定到不同 VBO 上,CPU 和 GPU 间通信也是很耗时的,所以人们设计了 顶点数组对象VAO,其负责在 GPU 端保存各顶点属性的绑定状态,只要一开始通过 glGenVertexArrays 申请一个VAO, 则在要进行相应绘制时只需要用 glBindVertexArray 绑定一次 VAO 告知接下来的VBO数据如何解读,就能自动绑定诸顶点属性,CPU 和 GPU 之间所需要的通信次数大大减少了.
VAO可以理解为保存了结构体的成员变量的定义, VBO负责指向结构体的内容, 在显卡对缓存进行类似readbuffer或者fopen后的指针操作时提供所需的依据;
VAO保存内容示意(仅方便理解, 不代表显卡实际上是如此组织存储的)

VAO对象 VBO标号 地址偏移 数据长度
\ 1 0 sizeof(GLfloat)
\ 1 sizeof(GLfloat) sizeof(GLfloat) * 3
\ 2 0 sizeof(GLfloat)

OpenGL/C++_学习笔记(三)绘制第一个图形_第2张图片

着色器的使用

同应用程序一样, 着色器程序在使用前也需要编程并进行编译和链接绑定, 用户对其的定义方式决定了着色器在渲染过程中实际的功能.

着色器程序设计

在本章节的应用中我们只需要绘制纯色三角形, 因此不需要复杂的着色器程序.

顶点着色器设计

在本例中顶点着色器负责向片元提交坐标数据.

#version 330 core

layout (location = 0) in vec3 aPos;

void main(){
    gl_Position = vec4(aPos, 1.0);
}
片元着色器设计

在本例中片元着色器负责给图元投射到的屏幕上的区域进行上色.

#version 330 core

out vec4 fcolor;

uniform vec4 color;

void main(){
    fcolor = color;
}
着色器程序的读取、编译、链接、使用

着色器程序的:

  1. 读取: 为了使应用程序代码更有可读性我们将着色器代码文本放入独立的文本文件中(通常以 .vs或 .fs作为后缀以区分是什么着色器), 在应用程序初始化时/需要使用该着色器前用 fopen 等文件读取方法读字符串到应用程序 再提交给 OpenGL 进行编译.
  2. 编译: 先通过 glCreateShader 向 OpenGL 告知着色器类型并申请着色器编号(类似 VBO 负责管理编译完成的着色器程序在显卡缓存中的地址), 然后将字符串/字符数组变量传入 glShaderSource 函数中向gl提交着色器代码资源, 最后等待 glCompileShader 函数编译完成, 此时可调用 glGetShaderiv 检查着色器程序是否链接成功.
  3. 链接: 先调用 glCreateProgram 获得着色器标号, 此后调用 glAttachShader 函数将着色器绑定到着色器程序对象上再使用 glLinkProgram 使显卡链接到着色器程序, 这样就完成了一个着色器程序对象的使用前准备工作, 此时可调用 glGetProgramiv 检查着色器程序是否链接成功.
  4. 使用: 上述步骤均按序完成, 在渲染目标对象前调用 glUseProgram 以绑定/激活着色器程序, 告诉显卡用该着色器程序去处理后续请求绘制的顶点数据; 此后调用 glDrawArrays 绘制(OpenGL 的绘制函数有很多种, 可以满足多样的绘制需求: OpenGL API介绍(三):画图函数)并用 glfwSwapBuffers 交换完成绘制的缓存到显示窗口, 如此就将三角形绘制到了窗口中.

第一个三角形效果图

OpenGL/C++_学习笔记(三)绘制第一个图形_第3张图片

三角形绘制完整代码示例

// OpenGL基本库
#include "include/glad/glad.h"
#include "include/GLFW/glfw3.h"

#include 
#include 
#include 
// 提供延时函数
#include 

// 窗口回调函数,每当窗口大小属性被修改时该函数被回调,参数即为该窗口新的大小
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
 // 未来会添加内容
}

// 事件检测函数
void process_Input(GLFWwindow* window)
{
 // 未来会添加内容
 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) // 按键检测,检测esc键是否按下
 {
  glfwSetWindowShouldClose(window, true);    // 设置对应窗口应该被关闭
 }
}

int main()
{
 // !初始化glfw窗口
 glfwInit();
 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);     // 指定OnencL的主版本号(和glad版本对应)
 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);     // 指定OpenGL的子版本号(和glad版本对应)
 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 设置使用OpenCL的核心模式(可编程渲染管道的模式)
 //创建窗口
 GLFWwindow* window = glfwCreateWindow(720, 720, "LearnOpenGL", NULL, NULL); // 参数含义:窗口宽,窗口高,窗口名称,窗口附加,窗口依赖,如果新建窗口失败返回NULL指针
 if (window == NULL)            // 如果出错,则退出程序
 {
  std::cout << "Failed to create GLFW window" << std::endl; // 报错信息
  glfwTerminate();           // 终止窗口进程
  return -1;
 }
 glfwMakeContextCurrent(window); // 将窗口的上下文环境设置为当前主线程的上下文环境 
 // !初始化glad
 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))  // 如果加载失败,退出程序
 {
  std::cout << "Failed to initialize GLAD" << std::endl;  // 报错信息
  return -1;
 }
 // 注册窗口回调函数
 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

 glClearColor(1.0f, 1.0f, 0.0f, 1.0f); // 设置清屏颜色为黄色

 //1
 //从文件中读取顶点着色器代码,并经字符串流存入字符串中
 std::string vertexCode_str;
 std::ifstream vertexCode_fstream;
 std::strstream vertexCode_strstream;

 try
 {
  vertexCode_fstream.open("triangle_vs.glsl");
  vertexCode_strstream << vertexCode_fstream.rdbuf() << std::ends;
  vertexCode_str = vertexCode_strstream.str();
  vertexCode_fstream.close();
 }
 catch (const std::exception&)
 {
  std::cerr << "triangle_vs.glsl" << " error!" << std::endl;;
 }

 //从文件中读取片元着色器代码,并经字符串流存入字符串中
 std::string fragmentCode_str;
 std::ifstream fragmentCode_fstream;
 std::strstream fragmentCode_strstream;

 try
 {
  fragmentCode_fstream.open("triangle_fs.glsl");
  fragmentCode_strstream << fragmentCode_fstream.rdbuf() << std::ends;
  fragmentCode_str = fragmentCode_strstream.str();
  fragmentCode_fstream.close();
 }
 catch (const std::exception&)
 {
  std::cerr << "triangle_fs.glsl" << " error!" << std::endl;;
 }

 //2
 int success;
 char infolog[512];
 // 编译顶点着色器
 const char* vertexCode_c_str = vertexCode_str.c_str();
 unsigned vertex_ID = glCreateShader(GL_VERTEX_SHADER);
 glShaderSource(vertex_ID, 1, &vertexCode_c_str, NULL);
 glCompileShader(vertex_ID);
 glGetShaderiv(vertex_ID, GL_COMPILE_STATUS, &success);
 if (!success)
 {
  glGetShaderInfoLog(vertex_ID, sizeof(infolog), NULL, infolog);
  std::cerr << "GL_VERTEX_SHADER:" << infolog << std::endl;
 }
 // 编译片元着色器
 const char* fragmentCode_c_str = fragmentCode_str.c_str();
 unsigned fragment_ID = glCreateShader(GL_FRAGMENT_SHADER);
 glShaderSource(fragment_ID, 1, &fragmentCode_c_str, NULL);
 glCompileShader(fragment_ID);
 glGetShaderiv(fragment_ID, GL_COMPILE_STATUS, &success); // 检查着色器是否编译成功
 if (!success)
 {
  glGetShaderInfoLog(fragment_ID, sizeof(infolog), NULL, infolog);
  std::cerr << "GL_FRAGMENT_SHADER:" << infolog << std::endl;
 }
 // 链接着色器程序
 GLuint progID = glCreateProgram();
 glAttachShader(progID, vertex_ID);
 glAttachShader(progID, fragment_ID);
 glLinkProgram(progID);
 glGetProgramiv(progID, GL_LINK_STATUS, &success);   // 检查着色器程序是否链接成功
 if (!success)
 {
  glGetProgramInfoLog(progID, 512, NULL, infolog);
  std::cerr << "glLinkProgram:" << infolog << std::endl;
 }
 // 删除着色器的函数
 // gl维护着着色器的引用计数, 若请求删除时还有着色器程序引用该着色器就只是标记为等待删除而不是直接删除
 glDeleteShader(vertex_ID);
 glDeleteShader(fragment_ID);

 // 三角形图元顶点信息(x,y,z)
 // 使用GLfloat便于gl跨平台编译: 每个操作系统/硬件环境对各变量类型的定义不一定完全一致
 GLfloat Vertices[] = { -0.5f, -0.5f, 0.0f,
       0.5f, -0.5f, 0.0f,
       0.0f, 0.5f, 0.0f
 };

 GLuint VAO;
 // 申请VAO
 glGenVertexArrays(1, &VAO);
 // 激活VAO
 glBindVertexArray(VAO);

 GLuint VBO;
 // 申请VBO
 glGenBuffers(1, &VBO);
 // 激活VBO
 glBindBuffer(GL_ARRAY_BUFFER, VBO);
 // 向VBO传入顶点数据
 // glBufferData()
 glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
 // 指定buffer中数据的读取方式(指针偏移)
 // 与glBufferData无先后关系
 // 与shader代码中的layout location对应
 // glVertexAttribPointer(index, size, type, normalized, strid, pointer)
 // index:  位于shader输入序列的位置  
 // size:  数据量
 // normalized: 是否归一化
 // strid:  到下一个顶点的位移量
 // pointer:  指针偏移量
 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 3, (void*)0);
 // 启用该属性, 否则着色器只会获得gl设定的默认值, 通常为0
 glEnableVertexAttribArray(0);

 glUseProgram(progID);

 // 蓝色
 // 颜色分量均在区间[0, 1]上, 渲染时再转换为对应像素编码(比如RGBA)
 GLfloat color[] = { 0 / 255, 255 / 255, 255 / 255, 1.0f };
 // glUniformXXX函数族
 glUniform4fv(glGetUniformLocation(progID, "color"), 1, color);
 // 渲染循环
 while (!glfwWindowShouldClose(window)) // 检测窗口是否该关闭
 {
  process_Input(window);    // 处理输入事件

  glClear(GL_COLOR_BUFFER_BIT);  // 清屏
  glDrawArrays(GL_TRIANGLES, 0, 3); // 
           // 
  glfwSwapBuffers(window);   // 将显示缓存和离屏缓存交换(显示离屏缓存中的内容)
  glfwPollEvents();     // 检查是否有事件触发(例如鼠标输入和键盘输入事件)
  Sleep(20);       // 防止本次刷新由于过于简单而刷新频率过高导致硬件占用过高
 }
 glfwTerminate();      // 终止进程
 return 0;
}

绘制第一个长方形(引入索引缓冲对象EBO)

必须说的是, 图元和其他类单元/单位的概念一样, 是组成复杂系统的最小对象, 因此绘制多边形可以通过重复绘制图元中的一种–即三角形实现.但正如前文所说, gl库提供了多样的绘制命令函数, 即使是重复绘制三角形, 在应用程序层面也有很多实现方法.

1. 直接套用微调上文绘制三角形的代码

四边形可以拆分成两个三角形, 因此我们可以直接在上文绘制三角形的代码的基础上修改几行, 提供两个三角形的顶点信息并向gl传递绘制两个拼接在一起的三角形的命令, 即可在画面上得到长方形.

// OpenGL基本库
#include "include/glad/glad.h"
#include "include/GLFW/glfw3.h"

#include 
#include 
#include 
// 提供延时函数
#include 

// 窗口回调函数,每当窗口大小属性被修改时该函数被回调,参数即为该窗口新的大小
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
 // 未来会添加内容
}

// 事件检测函数
void process_Input(GLFWwindow* window)
{
 // 未来会添加内容
 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) // 按键检测,检测esc键是否按下
 {
  glfwSetWindowShouldClose(window, true);    // 设置对应窗口应该被关闭
 }
}

int main()
{
 // !初始化glfw窗口
 glfwInit();
 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);     // 指定OnencL的主版本号(和glad版本对应)
 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);     // 指定OpenGL的子版本号(和glad版本对应)
 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 设置使用OpenCL的核心模式(可编程渲染管道的模式)
 //创建窗口
 GLFWwindow* window = glfwCreateWindow(720, 720, "LearnOpenGL", NULL, NULL); // 参数含义:窗口宽,窗口高,窗口名称,窗口附加,窗口依赖,如果新建窗口失败返回NULL指针
 if (window == NULL)            // 如果出错,则退出程序
 {
  std::cout << "Failed to create GLFW window" << std::endl; // 报错信息
  glfwTerminate();           // 终止窗口进程
  return -1;
 }
 glfwMakeContextCurrent(window); // 将窗口的上下文环境设置为当前主线程的上下文环境 
 // !初始化glad
 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))  // 如果加载失败,退出程序
 {
  std::cout << "Failed to initialize GLAD" << std::endl;  // 报错信息
  return -1;
 }
 // 注册窗口回调函数
 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

 glClearColor(1.0f, 1.0f, 0.0f, 1.0f); // 设置清屏颜色为黄色

 //1
 //从文件中读取顶点着色器代码,并经字符串流存入字符串中
 std::string vertexCode_str;
 std::ifstream vertexCode_fstream;
 std::strstream vertexCode_strstream;

 try
 {
  vertexCode_fstream.open("triangle_vs.glsl");
  vertexCode_strstream << vertexCode_fstream.rdbuf() << std::ends;
  vertexCode_str = vertexCode_strstream.str();
  vertexCode_fstream.close();
 }
 catch (const std::exception&)
 {
  std::cerr << "triangle_vs.glsl" << " error!" << std::endl;;
 }

 //从文件中读取片元着色器代码,并经字符串流存入字符串中
 std::string fragmentCode_str;
 std::ifstream fragmentCode_fstream;
 std::strstream fragmentCode_strstream;

 try
 {
  fragmentCode_fstream.open("triangle_fs.glsl");
  fragmentCode_strstream << fragmentCode_fstream.rdbuf() << std::ends;
  fragmentCode_str = fragmentCode_strstream.str();
  fragmentCode_fstream.close();
 }
 catch (const std::exception&)
 {
  std::cerr << "triangle_fs.glsl" << " error!" << std::endl;;
 }

 //2
 int success;
 char infolog[512];
 // 编译顶点着色器
 const char* vertexCode_c_str = vertexCode_str.c_str();
 unsigned vertex_ID = glCreateShader(GL_VERTEX_SHADER);
 glShaderSource(vertex_ID, 1, &vertexCode_c_str, NULL);
 glCompileShader(vertex_ID);
 glGetShaderiv(vertex_ID, GL_COMPILE_STATUS, &success);
 if (!success)
 {
  glGetShaderInfoLog(vertex_ID, sizeof(infolog), NULL, infolog);
  std::cerr << "GL_VERTEX_SHADER:" << infolog << std::endl;
 }
 // 编译片元着色器
 const char* fragmentCode_c_str = fragmentCode_str.c_str();
 unsigned fragment_ID = glCreateShader(GL_FRAGMENT_SHADER);
 glShaderSource(fragment_ID, 1, &fragmentCode_c_str, NULL);
 glCompileShader(fragment_ID);
 glGetShaderiv(fragment_ID, GL_COMPILE_STATUS, &success); // 检查着色器是否编译成功
 if (!success)
 {
  glGetShaderInfoLog(fragment_ID, sizeof(infolog), NULL, infolog);
  std::cerr << "GL_FRAGMENT_SHADER:" << infolog << std::endl;
 }
 // 链接着色器程序
 GLuint progID = glCreateProgram();
 glAttachShader(progID, vertex_ID);
 glAttachShader(progID, fragment_ID);
 glLinkProgram(progID);
 glGetProgramiv(progID, GL_LINK_STATUS, &success);   // 检查着色器程序是否链接成功
 if (!success)
 {
  glGetProgramInfoLog(progID, 512, NULL, infolog);
  std::cerr << "glLinkProgram:" << infolog << std::endl;
 }
 // 删除着色器的函数
 // gl维护着着色器的引用计数, 若请求删除时还有着色器程序引用该着色器就只是标记为等待删除而不是直接删除
 glDeleteShader(vertex_ID);
 glDeleteShader(fragment_ID);

 // 三角形图元顶点信息(x,y,z)
 // 使用GLfloat便于gl跨平台编译: 每个操作系统/硬件环境对各变量类型的定义不一定完全一致
 //GLfloat Vertices[] = { -0.5f, -0.5f, 0.0f,
 //      0.5f, -0.5f, 0.0f,
 //      0.0f, 0.5f, 0.0f
 //};
 // 长方形图元顶点信息(非索引绘制模式)
 GLfloat Vertices[] = {
  // 1 (左上)
  -0.8f, -0.4f, 0.0f,
  0.8f, 0.4f, 0.0f,
  -0.8f, 0.4f, 0.0f,
  // 2 (右下)
  0.8f, 0.4f, 0.0f,
  -0.8f, -0.4f, 0.0f,
  0.8f, -0.4f, 0.0f
 };

 GLuint VAO;
 // 申请VAO
 glGenVertexArrays(1, &VAO);
 // 激活VAO
 glBindVertexArray(VAO);

 GLuint VBO;
 // 申请VBO
 glGenBuffers(1, &VBO);
 // 激活VBO
 glBindBuffer(GL_ARRAY_BUFFER, VBO);
 // 向VBO传入顶点数据
 // glBufferData()
 glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
 // 指定buffer中数据的读取方式(指针偏移)
 // 与glBufferData无先后关系
 // 与shader代码中的layout location对应
 // glVertexAttribPointer(index, size, type, normalized, strid, pointer)
 // index:  位于shader输入序列的位置  
 // size:  数据量
 // normalized: 是否归一化
 // strid:  到下一个顶点的位移量
 // pointer:  指针偏移量
 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 3, (void*)0);
 // 启用该属性, 否则着色器只会获得gl设定的默认值, 通常为0
 glEnableVertexAttribArray(0);

 // 告诉显卡接下来使用该着色器程序进行渲染
 glUseProgram(progID);

 // 颜色分量均在区间[0, 1]上, 渲染时再转换为对应像素编码(比如RGBA)
 // 蓝色
 GLfloat color[] = { 0 / 255, 255 / 255, 255 / 255, 1.0f };
 // glUniformXXX函数族
 glUniform4fv(glGetUniformLocation(progID, "color"), 1, color);
 // 渲染循环
 while (!glfwWindowShouldClose(window)) // 检测窗口是否该关闭
 {
  process_Input(window);    // 处理输入事件

  glClear(GL_COLOR_BUFFER_BIT);  // 清屏
  glDrawArrays(GL_TRIANGLES, 0, 6); // glDrawXXX绘制函数族
           // glDrawArrays(mode, first, count)
           // mode:  顶点组织图元的模式
           // first: 第一个顶点位于顶点缓存中的位置
           // count: 参与绘制的顶点数
  glfwSwapBuffers(window);   // 将显示缓存和离屏缓存交换(显示离屏缓存中的内容)
  glfwPollEvents();     // 检查是否有事件触发(例如鼠标输入和键盘输入事件)
  Sleep(20);       // 防止本次刷新由于过于简单而刷新频率过高导致硬件占用过高
 }
 glfwTerminate();      // 终止进程
 return 0;
}

直接套用代码进行重复绘制的效果图

OpenGL/C++_学习笔记(三)绘制第一个图形_第4张图片

2. 使用索引/元素(IBO/EBO)进行优化

对于重复绘制尤其是模型顶点会因为图元共点共线而重复使用的情况下, 顶点数组会消耗大量的存储空间存入重复的顶点信息, 因此引入索引缓存对象(Index Buffer Object,IBO)或称为元素缓存大小(Element Buffer Object,EBO)

EBO绘制长方形效果图

同普通顶点绘制的结果一样, 因此不占篇幅进行重复展示.

EBO绘制完整代码示例
// OpenGL基本库
# include "include/glad/glad.h"
# include "include/GLFW/glfw3.h"

# include 
# include 
# include 
// 提供延时函数
# include 

// 窗口回调函数,每当窗口大小属性被修改时该函数被回调,参数即为该窗口新的大小
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
 // 未来会添加内容
}

// 事件检测函数
void process_Input(GLFWwindow* window)
{
 // 未来会添加内容
 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) // 按键检测,检测esc键是否按下
 {
  glfwSetWindowShouldClose(window, true);    // 设置对应窗口应该被关闭
 }
}

int main()
{
 // !初始化glfw窗口
 glfwInit();
 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);     // 指定OnencL的主版本号(和glad版本对应)
 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);     // 指定OpenGL的子版本号(和glad版本对应)
 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 设置使用OpenCL的核心模式(可编程渲染管道的模式)
 //创建窗口
 GLFWwindow* window = glfwCreateWindow(720, 720, "LearnOpenGL", NULL, NULL); // 参数含义:窗口宽,窗口高,窗口名称,窗口附加,窗口依赖,如果新建窗口失败返回NULL指针
 if (window == NULL)            // 如果出错,则退出程序
 {
  std::cout << "Failed to create GLFW window" << std::endl; // 报错信息
  glfwTerminate();           // 终止窗口进程
  return -1;
 }
 glfwMakeContextCurrent(window); // 将窗口的上下文环境设置为当前主线程的上下文环境
 // !初始化glad
 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))  // 如果加载失败,退出程序
 {
  std::cout << "Failed to initialize GLAD" << std::endl;  // 报错信息
  return -1;
 }
 // 注册窗口回调函数
 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

 glClearColor(1.0f, 1.0f, 0.0f, 1.0f); // 设置清屏颜色为黄色

 //1
 //从文件中读取顶点着色器代码,并经字符串流存入字符串中
 std::string vertexCode_str;
 std::ifstream vertexCode_fstream;
 std::strstream vertexCode_strstream;

 try
 {
  vertexCode_fstream.open("triangle_vs.glsl");
  vertexCode_strstream << vertexCode_fstream.rdbuf() << std::ends;
  vertexCode_str = vertexCode_strstream.str();
  vertexCode_fstream.close();
 }
 catch (const std::exception&)
 {
  std::cerr << "triangle_vs.glsl" << " error!" << std::endl;;
 }

 //从文件中读取片元着色器代码,并经字符串流存入字符串中
 std::string fragmentCode_str;
 std::ifstream fragmentCode_fstream;
 std::strstream fragmentCode_strstream;

 try
 {
  fragmentCode_fstream.open("triangle_fs.glsl");
  fragmentCode_strstream << fragmentCode_fstream.rdbuf() << std::ends;
  fragmentCode_str = fragmentCode_strstream.str();
  fragmentCode_fstream.close();
 }
 catch (const std::exception&)
 {
  std::cerr << "triangle_fs.glsl" << " error!" << std::endl;;
 }

 //2
 int success;
 char infolog[512];
 // 编译顶点着色器
 const char* vertexCode_c_str = vertexCode_str.c_str();
 unsigned vertex_ID = glCreateShader(GL_VERTEX_SHADER);
 glShaderSource(vertex_ID, 1, &vertexCode_c_str, NULL);
 glCompileShader(vertex_ID);
 glGetShaderiv(vertex_ID, GL_COMPILE_STATUS, &success);
 if (!success)
 {
  glGetShaderInfoLog(vertex_ID, sizeof(infolog), NULL, infolog);
  std::cerr << "GL_VERTEX_SHADER:" << infolog << std::endl;
 }
 // 编译片元着色器
 const char* fragmentCode_c_str = fragmentCode_str.c_str();
 unsigned fragment_ID = glCreateShader(GL_FRAGMENT_SHADER);
 glShaderSource(fragment_ID, 1, &fragmentCode_c_str, NULL);
 glCompileShader(fragment_ID);
 glGetShaderiv(fragment_ID, GL_COMPILE_STATUS, &success); // 检查着色器是否编译成功
 if (!success)
 {
  glGetShaderInfoLog(fragment_ID, sizeof(infolog), NULL, infolog);
  std::cerr << "GL_FRAGMENT_SHADER:" << infolog << std::endl;
 }
 // 链接着色器程序
 GLuint progID = glCreateProgram();
 glAttachShader(progID, vertex_ID);
 glAttachShader(progID, fragment_ID);
 glLinkProgram(progID);
 glGetProgramiv(progID, GL_LINK_STATUS, &success);   // 检查着色器程序是否链接成功
 if (!success)
 {
  glGetProgramInfoLog(progID, 512, NULL, infolog);
  std::cerr << "glLinkProgram:" << infolog << std::endl;
 }
 // 删除着色器的函数
 // gl维护着着色器的引用计数, 若请求删除时还有着色器程序引用该着色器就只是标记为等待删除而不是直接删除
 glDeleteShader(vertex_ID);
 glDeleteShader(fragment_ID);

 // 三角形图元顶点信息(x,y,z)
 // 使用GLfloat便于gl跨平台编译: 每个操作系统/硬件环境对各变量类型的定义不一定完全一致
 //GLfloat Vertices[] = { -0.5f, -0.5f, 0.0f,
 //      0.5f, -0.5f, 0.0f,
 //      0.0f, 0.5f, 0.0f
 //};
 // 长方形图元顶点信息(非索引绘制模式)
 //GLfloat Vertices[] = {
 // // 1 (左上)
 // -0.8f, -0.4f, 0.0f,
 // 0.8f, 0.4f, 0.0f,
 // -0.8f, 0.4f, 0.0f,
 // // 2 (右下)
 // 0.8f, 0.4f, 0.0f,
 // -0.8f, -0.4f, 0.0f,
 // 0.8f, -0.4f, 0.0f
 //};
 // 长方形图元顶点信息(索引绘制)
 GLfloat Vertices[] = {
  // 1 (左下)
  -0.8f, -0.4f, 0.0f,
  // 2 (左上)
  -0.8f, 0.4f, 0.0f,
  // 3 (右上)
  0.8f, 0.4f, 0.0f,
  // 4 (右下)
  0.8f, -0.4f, 0.0f
 };
 GLuint indicator[] = {
  0, 1, 2,
  2, 3, 0
 };
 GLuint EBO;

 GLuint VAO;
 // 申请VAO
 glGenVertexArrays(1, &VAO);
 // 激活VAO
 glBindVertexArray(VAO);

 GLuint VBO;
 // 申请VBO
 glGenBuffers(1, &VBO);
 // 激活VBO
 glBindBuffer(GL_ARRAY_BUFFER, VBO);
 // 向VBO传入顶点数据
 // glBufferData()
 glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
 // 指定buffer中数据的读取方式(指针偏移)
 // 与glBufferData无先后关系
 // 与shader代码中的layout location对应
 // glVertexAttribPointer(index, size, type, normalized, strid, pointer)
 // index:  位于shader输入序列的位置  
 // size:  数据量
 // normalized: 是否归一化
 // strid:  到下一个顶点的位移量
 // pointer:  指针偏移量
 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 3, (void*)0);
 // 启用该属性, 否则着色器只会获得gl设定的默认值, 通常为0
 glEnableVertexAttribArray(0);

 glGenBuffers(1, &EBO);
 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indicator), indicator, GL_STATIC_DRAW);

 // 告诉显卡接下来使用该着色器程序进行渲染
 glUseProgram(progID);

 // 颜色分量均在区间[0, 1]上, 渲染时再转换为对应像素编码(比如RGBA)
 // 蓝色
 GLfloat color[] = { 0 / 255, 255 / 255, 255 / 255, 1.0f };
 // glUniformXXX函数族
 glUniform4fv(glGetUniformLocation(progID, "color"), 1, color);
 // 渲染循环
 while (!glfwWindowShouldClose(window)) // 检测窗口是否该关闭
 {
  process_Input(window);    // 处理输入事件

  glClear(GL_COLOR_BUFFER_BIT);  // 清屏
  // glDrawXXX绘制函数族
  //glDrawArrays(GL_TRIANGLES, 0, 6); // glDrawArrays(mode, first, count)
  //         // mode:  顶点组织图元的模式
  //         // first: 第一个顶点位于顶点缓存中的位置
  //         // count: 参与绘制的顶点数
  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)0); // glDrawElements(mode, count, type, indices)
                 // mode: 顶点组织图元的模式
                 // count: 实际构成图元的顶点数(含重复)
                 // type: 索引数组元素的数据类型
                 // indices: 索引数据起始地址在索引缓存EBO中的偏移量
  glfwSwapBuffers(window);   // 将显示缓存和离屏缓存交换(显示离屏缓存中的内容)
  glfwPollEvents();     // 检查是否有事件触发(例如鼠标输入和键盘输入事件)
  Sleep(20);       // 防止本次刷新由于过于简单而刷新频率过高导致硬件占用过高
 }
 glfwTerminate();      // 终止进程
 return 0;
}
glDrawElements(mode, count, type, indices)函数作用的一些奇怪猜想

先介绍一下glDrawElements(mode, count, type, indices)中四个参数含义, 分别是:

  1. mode: 同glDrawArrays中的mode参数, 指明顶点组织图元的模式
  2. count: 实际构成图元的顶点数(含重复)
  3. type: 索引数组元素的数据类型
  4. indices: 索引数据起始地址在索引缓存EBO中的偏移量

然后, 众所周知顶点是绘制模型的核心内容, 各个顶点是同等重要的且有精度需求, 在显存中要能随机读写(占用内存等长), 因此设计了EBO来节省顶点的内存占用;
那如果顶点数特别多呢, EBO的内存占用也会随之变大, 毕竟也放在显卡内存里, 也会占用VBO的空间, 因此可以更改EBO内存储信息的type(byte/short/unit);
那为什么要在绘制的时候才传入指明类型呢, 为什么不在EBO存入显存时就单独用一个函数定义EBO数据类型呢就?
然后, 根据以上事实, 我在这里给出GL在EBO相关作用的设计函数功能的实际用途的个人猜想:
由于节省空间多出现在顶点数多显存占用大的情况下, 这时我们假设在buffer传入时就指定EBO全是short型的数据, 那如果顶点数造成short上溢呢, 后面的图元又不能被抛弃, 这时EBO在绘制时如此设计绘制函数的意义就出现了:
对byte能保存各顶点索引数据的图元, 我们在绘制中指定数据为byte以及顶点数据起止

glDrawElements(GL_TRIANGLES, 256, GL_UNSIGNED_BYTE, (void*) 0 );

对byte上溢的索引数字(257), 采用short来存储图元索引, 并给出short型EBO在缓存中的地址偏移; 以此类推, 最后采用 GL_UNSIGNED_INT 来保存前两者都不能保存的数据, 如此设计, 以绘制时的硬件通讯费时(从一次绘制变成最多3次)为代价换来的是数据数量大的 VBO 加上 EBO 统共占用的显存变少了.
以上, 是我对gl对EBO使用方式和glDrawElements函数定义的设计的一种功能性猜想:
在顶点数过大而导致的VBO显存占用过高情况下的优化方案(时间换空间).

绘制多实例的优化(XXXInstanced绘制函数相关)

在实际应用中会出现绘制多个模型相同的对象的需求(只需要模型内部各顶点相对位置信息一致, 可以位置不同, 大小不同, 旋转角度不同, 纹理不同), 重复调用绘制指令单独绘制会损失大量时间在 cpu 和 gpu 进行通信上. 因此引入了多实例绘制以及顶点数据的新作用.

功能介绍

glVertexAttribDivisor函数是一个控制顶点数据在绘制时更新频率的函数, 该函数有两个参数:

glVertexAttribDivisor(index, divisor);  //index:    顶点属性的编号
                                        //divisor:  增量因子,表示每个顶点属性的增量步长

例如,如果我们要在渲染100个实例时使用第0个顶点属性,并且希望每隔10个实例更新一次属性,则可以使用:
glVertexAttribDivisor(0, 10);

这样第0个属性将会在渲染第0个实例时使用,在渲染第10个实例时使用第1个属性,在渲染第20个实例时使用第2个属性,依次类推.

增量因子 divisor 可以用于实现实例化渲染,这是一种高效的渲染方式,可以用一组顶点数据来绘制多个物体. 每个实例都有自己独特的属性数据,通过设置增量因子来确保每个实例都有自己独特的属性数据.
比如说,你可以用一组顶点数据来绘制100个矩形,每个矩形都有不同的颜色和位置.

glDrawElementsInstanced(mode, count, type, indices, primCount);
// mode:      绘制图元类型
// count:     绘制一个实例需要的顶点数(含重复)
// type:      索引数组元素数据类型
// indices:   索引偏移量
// primCount: 需要绘制的实例数

顶点着色器

layout (location = index1) in vec4 intancePos;   // 实例世界空间坐标
layout (location = index2) in float intancescale;   // 实例缩放比例
layout (location = 3) in vec4 intanceColor;
...
gl_Position = vec4((aPos * intancescale) + intancePos, 1.0);     // 此时aPos变成了模型空间内部各顶点相对坐标原点的偏移量

多图形实例绘制效果图

OpenGL/C++_学习笔记(三)绘制第一个图形_第5张图片

多图形对象绘制完整代码示例(含着色器代码)

C++

// OpenGL基本库
#include "include/glad/glad.h"
#include "include/GLFW/glfw3.h"

#include 
#include 
#include 
// 提供延时函数
#include 

// 窗口回调函数,每当窗口大小属性被修改时该函数被回调,参数即为该窗口新的大小
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
 // 未来会添加内容
}

// 事件检测函数
void process_Input(GLFWwindow* window)
{
 // 未来会添加内容
 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) // 按键检测,检测esc键是否按下
 {
  glfwSetWindowShouldClose(window, true);    // 设置对应窗口应该被关闭
 }
}

int main()
{
 // !初始化glfw窗口
 glfwInit();
 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);     // 指定OnencL的主版本号(和glad版本对应)
 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);     // 指定OpenGL的子版本号(和glad版本对应)
 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 设置使用OpenCL的核心模式(可编程渲染管道的模式)
 //创建窗口
 GLFWwindow* window = glfwCreateWindow(720, 720, "LearnOpenGL", NULL, NULL); // 参数含义:窗口宽,窗口高,窗口名称,窗口附加,窗口依赖,如果新建窗口失败返回NULL指针
 if (window == NULL)            // 如果出错,则退出程序
 {
  std::cout << "Failed to create GLFW window" << std::endl; // 报错信息
  glfwTerminate();           // 终止窗口进程
  return -1;
 }
 glfwMakeContextCurrent(window); // 将窗口的上下文环境设置为当前主线程的上下文环境 
 // !初始化glad
 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))  // 如果加载失败,退出程序
 {
  std::cout << "Failed to initialize GLAD" << std::endl;  // 报错信息
  return -1;
 }
 // 注册窗口回调函数
 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

 glClearColor(1.0f, 1.0f, 0.0f, 1.0f); // 设置清屏颜色为黄色

 //1
 //从文件中读取顶点着色器代码,并经字符串流存入字符串中
 std::string vertexCode_str;
 std::ifstream vertexCode_fstream;
 std::strstream vertexCode_strstream;

 try
 {
  vertexCode_fstream.open("triangle_vs.glsl");
  vertexCode_strstream << vertexCode_fstream.rdbuf() << std::ends;
  vertexCode_str = vertexCode_strstream.str();
  vertexCode_fstream.close();
 }
 catch (const std::exception&)
 {
  std::cerr << "triangle_vs.glsl" << " error!" << std::endl;;
 }

 //从文件中读取片元着色器代码,并经字符串流存入字符串中
 std::string fragmentCode_str;
 std::ifstream fragmentCode_fstream;
 std::strstream fragmentCode_strstream;

 try
 {
  fragmentCode_fstream.open("triangle_fs.glsl");
  fragmentCode_strstream << fragmentCode_fstream.rdbuf() << std::ends;
  fragmentCode_str = fragmentCode_strstream.str();
  fragmentCode_fstream.close();
 }
 catch (const std::exception&)
 {
  std::cerr << "triangle_fs.glsl" << " error!" << std::endl;;
 }

 //2
 int success;
 char infolog[512];
 // 编译顶点着色器
 const char* vertexCode_c_str = vertexCode_str.c_str();
 unsigned vertex_ID = glCreateShader(GL_VERTEX_SHADER);
 glShaderSource(vertex_ID, 1, &vertexCode_c_str, NULL);
 glCompileShader(vertex_ID);
 glGetShaderiv(vertex_ID, GL_COMPILE_STATUS, &success);
 if (!success)
 {
  glGetShaderInfoLog(vertex_ID, sizeof(infolog), NULL, infolog);
  std::cerr << "GL_VERTEX_SHADER:" << infolog << std::endl;
 }
 // 编译片元着色器
 const char* fragmentCode_c_str = fragmentCode_str.c_str();
 unsigned fragment_ID = glCreateShader(GL_FRAGMENT_SHADER);
 glShaderSource(fragment_ID, 1, &fragmentCode_c_str, NULL);
 glCompileShader(fragment_ID);
 glGetShaderiv(fragment_ID, GL_COMPILE_STATUS, &success); // 检查着色器是否编译成功
 if (!success)
 {
  glGetShaderInfoLog(fragment_ID, sizeof(infolog), NULL, infolog);
  std::cerr << "GL_FRAGMENT_SHADER:" << infolog << std::endl;
 }
 // 链接着色器程序
 GLuint progID = glCreateProgram();
 glAttachShader(progID, vertex_ID);
 glAttachShader(progID, fragment_ID);
 glLinkProgram(progID);
 glGetProgramiv(progID, GL_LINK_STATUS, &success);   // 检查着色器程序是否链接成功
 if (!success)
 {
  glGetProgramInfoLog(progID, 512, NULL, infolog);
  std::cerr << "glLinkProgram:" << infolog << std::endl;
 }
 // 删除着色器的函数
 // gl维护着着色器的引用计数, 若请求删除时还有着色器程序引用该着色器就只是标记为等待删除而不是直接删除
 glDeleteShader(vertex_ID);
 glDeleteShader(fragment_ID);

 // 三角形图元顶点信息(x,y,z)
 // 使用GLfloat便于gl跨平台编译: 每个操作系统/硬件环境对各变量类型的定义不一定完全一致
 //GLfloat Vertices[] = { -0.5f, -0.5f, 0.0f,
 //      0.5f, -0.5f, 0.0f,
 //      0.0f, 0.5f, 0.0f
 //};
 // 长方形图元顶点信息(非索引绘制模式)
 //GLfloat Vertices[] = {
 // // 1 (左上)
 // -0.8f, -0.4f, 0.0f,
 // 0.8f, 0.4f, 0.0f,
 // -0.8f, 0.4f, 0.0f,
 // // 2 (右下)
 // 0.8f, 0.4f, 0.0f,
 // -0.8f, -0.4f, 0.0f,
 // 0.8f, -0.4f, 0.0f
 //};
 // 长方形图元顶点信息(索引绘制)
 GLfloat Vertices[] = {
  // 1 (左下)
  -0.1f, -0.2f, 0.0f,
  // 2 (左上)
  -0.1f, 0.2f, 0.0f,
  // 3 (右上)
  0.1f, 0.2f, 0.0f,
  // 4 (右下)
  0.1f, -0.2f, 0.0f
 };
 GLuint indicator[] = {
  0, 1, 2,
  2, 3, 0
 };
 GLuint EBO;

 GLuint VAO;
 // 申请VAO
 glGenVertexArrays(1, &VAO);
 // 激活VAO
 glBindVertexArray(VAO);

 // 申请EBO
 glGenBuffers(1, &EBO);
 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indicator), indicator, GL_STATIC_DRAW);

 GLuint VBO;
 // 申请VBO
 glGenBuffers(1, &VBO);
 // 激活VBO
 glBindBuffer(GL_ARRAY_BUFFER, VBO);
 // 向VBO传入顶点数据
 // glBufferData()
 glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
 // 指定buffer中数据的读取方式(指针偏移)
 // 与glBufferData无先后关系
 // 与shader代码中的layout location对应
 // glVertexAttribPointer(index, size, type, normalized, strid, pointer)
 // index:  位于shader输入序列的位置  
 // size:  数据量
 // normalized: 是否归一化
 // strid:  到下一个顶点的位移量
 // pointer:  指针偏移量
 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 3, (void*)0);
 // 启用该属性, 否则着色器只会获得gl设定的默认值, 通常为0
 glEnableVertexAttribArray(0);

 GLuint intancePos_VBO;
 glGenBuffers(1, &intancePos_VBO);
 glBindBuffer(GL_ARRAY_BUFFER, intancePos_VBO);
 GLfloat intancePos[] = {
  -0.45f, 0.0f, 0.0f,
  -0.15, 0.0f, 0.0f, 
  0.15, 0.0f, 0.0f, 
  0.45f, 0.0f, 0.0f, 

  -0.45f, 0.0f, 0.0f, 
  -0.15, 0.0f, 0.0f, 
  0.15, 0.0f, 0.0f, 
  0.45f, 0.0f, 0.0f, 
 };
 glBufferData(GL_ARRAY_BUFFER, sizeof(intancePos), intancePos, GL_STATIC_DRAW);
 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 3, (void*)0);
 glEnableVertexAttribArray(1);
 glVertexAttribDivisor(1, 1);

 GLuint scale_VBO;
 glGenBuffers(1, &scale_VBO);
 glBindBuffer(GL_ARRAY_BUFFER, scale_VBO);
 GLfloat intanceScale[] = {
  1.0f,
  1.0f,
  1.0f,
  1.0f,

  0.8f,
  0.8f,
  0.8f,
  0.8f
 };
 glBufferData(GL_ARRAY_BUFFER, sizeof(intanceScale), intanceScale, GL_STATIC_DRAW);
 glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 1, (void*)0);
 glEnableVertexAttribArray(2);
 glVertexAttribDivisor(2, 1);

 GLuint color_VBO;
 glGenBuffers(1, &color_VBO);
 glBindBuffer(GL_ARRAY_BUFFER, color_VBO);
 GLfloat intanceColor[] = {
  0 / 255, 255 / 255, 255 / 255, 1.0f,
  0 / 255, 255 / 255, 255 / 255, 1.0f,
  0 / 255, 255 / 255, 255 / 255, 1.0f,
  0 / 255, 255 / 255, 255 / 255, 1.0f,

  0 / 255, 0 / 255, 0 / 255, 1.0f,
  0 / 255, 0 / 255, 0 / 255, 1.0f,
  0 / 255, 0 / 255, 0 / 255, 1.0f,
  0 / 255, 0 / 255, 0 / 255, 1.0f
 };
 glBufferData(GL_ARRAY_BUFFER, sizeof(intanceColor), intanceColor, GL_STATIC_DRAW);
 glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, (void*)0);
 glEnableVertexAttribArray(3);
 glVertexAttribDivisor(3, 1);

 // 告诉显卡接下来使用该着色器程序进行渲染
 glUseProgram(progID);

 // 颜色分量均在区间[0, 1]上, 渲染时再转换为对应像素编码(比如RGBA)
  蓝色
 //GLfloat color[] = { 0 / 255, 255 / 255, 255 / 255, 1.0f };
  glUniformXXX函数族
 //glUniform4fv(glGetUniformLocation(progID, "color"), 1, color);
 // 渲染循环
 while (!glfwWindowShouldClose(window)) // 检测窗口是否该关闭
 {
  process_Input(window);    // 处理输入事件

  glClear(GL_COLOR_BUFFER_BIT);  // 清屏
  // glDrawXXX绘制函数族
  //glDrawArrays(GL_TRIANGLES, 0, 6); // glDrawArrays(mode, first, count)
  //         // mode:  顶点组织图元的模式
  //         // first: 第一个顶点位于顶点缓存中的位置
  //         // count: 参与绘制的顶点数
  //glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)0); // glDrawElements(mode, count, type, indices)
  //               // mode: 顶点组织图元的模式
  //               // count: 实际构成图元的顶点数(含重复)
  //               // type: 索引数组元素的数据类型
  //               // indices: 索引数据起始地址在索引缓存EBO中的偏移量
  glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (void*)0, 8);
  glfwSwapBuffers(window);   // 将显示缓存和离屏缓存交换(显示离屏缓存中的内容)
  glfwPollEvents();     // 检查是否有事件触发(例如鼠标输入和键盘输入事件)
  Sleep(20);       // 防止本次刷新由于过于简单而刷新频率过高导致硬件占用过高
 }
 glfwTerminate();      // 终止进程
 return 0;
}

顶点着色器

#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 intancePos;
layout (location = 2) in float intancescale;
layout (location = 3) in vec4 intanceColor;

out vec4 color;

void main(){
 gl_Position = vec4((aPos * intancescale) + intancePos, 1.0);
 color = intanceColor;

}

片元着色器

#version 330 core

out vec4 fcolor;

// layout (location = 0) uniform vec4 color;
in vec4 color;

void main(){
    fcolor = color;
}

一些基础绘制相关杂项

线框模式(Wireframe Mode)

要想用线框模式绘制你的三角形,你可以通过glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)函数配置OpenGL如何绘制图元。

  1. 第一个参数表示我们打算将其应用到所有的三角形的正面和背面;
  2. 第二个参数告诉我们用线来绘制;

之后的绘制调用会一直以线框模式绘制三角形,直到我们用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)将其设置回默认模式。

本文绘制内容在线框模式下的绘制结果

OpenGL/C++_学习笔记(三)绘制第一个图形_第6张图片


下一篇: OpenGL/C++_学习笔记(四)三维空间与摄像头概念

你可能感兴趣的:(OpenGL入门笔记,c++,学习,笔记,图形渲染)