OpenGL——三角形图元的绘制

三角形绘制

图元介绍

三角形是三个顶点的集合组成,当分别渲染多个三角形的时候,每个三角形与其他三角形完全独立;三角形的渲染时通过三个顶点到屏幕的投影以及三条边的链接来完成。

OpenGL——三角形图元的绘制_第1张图片

如果两个三角形共享一个边,那么不可能有任何采样值同时位于这两个三角形之内,OpenGL可以支持多种不同的光栅化算法,但是在共享边上的像素值设置却有非常严格的规定:

1. 两个三角形的共享边上的像素值因为同时被两者所覆盖,因此不可能不受到光照计算的影响;

2. 两个三角形的共享边上的像素值,不可能受到多于一个三角形的光照计算的影响。

三角形环绕方式

OpenGL——三角形图元的绘制_第2张图片

在绘制第⼀个三⻆形时,线条是按照从V0-V1,再到V2。最后再回到V0的一个闭合三⻆角形。 这个是沿着顶点顺时针方向。这种顺序与⽅向结合来指定顶点的方式称为环绕。在默认情况下,OpenGL 认为具有逆时针方向环绕的多边形为正面. 这就意味着上图左边是正面,右边是反⾯.

glFrontFace(GL_CW);

GL_CW:告诉OpenGL 顺时针环绕的多边形为正面;
GL_CCW:告诉OpenGL 逆时针环绕的多边形为正面

对于一个由不透明的且方向一致的多边形组成的、完全封闭的模型表面来说,它的所有背面多边形都是不可见的,那么可以开启裁剪来直接抛弃OpenGL中的背面多边形:

glEnable(GL_CULL_FACE);
glCullMode(GL_BACK);

控制多边形背面和正面的绘制方式:

glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);

顶点数组对象

顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。

           OpenGL——三角形图元的绘制_第3张图片

一个顶点数组对象会储存以下这些内容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
unsigned int VAO;
glGenVertexArrays(1, &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);
someOpenGLFunctionThatDrawsOurTriangle();

使用非索引绘制三角形

1. 准备顶点数据

开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是3D坐标(x、y和z)。OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。

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   // 左上角
};

标准化设备坐标(Normalized Device Coordinates, NDC)

一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):

OpenGL——三角形图元的绘制_第4张图片

与通常的屏幕坐标不同,y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。最终你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。

你的标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。

2. 顶点数据输入

定义顶点数据之后,会将它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器;它会在GPU上创建内存用于储存顶点数据,当然还是配置如何解析这个内存数据,并且指定其如何发送给显卡,接着顶点着色器会会处理我们在内存中指定数量的顶点。

通过顶点缓冲对象(Vertex Buffer Objects,VBO)管理这个内存,它会在GPU内存中储存大量的顶点;使用这个缓冲对象的好处时可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次;从CPU把数据发送到显卡相对较慢,所以只要可能都是尝试尽量一次性发送尽可能多的数据;当数据发送至显卡的内存中后,顶点着色器几乎能理级访问顶点,这是很快的过程。顶点缓冲对象的创建方式如下:

unsigned int VBO;
glGenBuffers(1,&VBO);

//顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER
glBindBuffer(GL_ARRAY_BUFFER,VBO);

//复制顶点数据到缓冲的内存中
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);

glBufferData 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:

  • GL_STATIC_DRAW :数据不会或几乎不会改变。
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变。

3.链接顶点属性

顶点着色器允许我们指定任何的以顶点属性为形式的输入,我们必须在渲染之前告诉OpenGl 改如何解析这个顶点数据:

           OpenGL——三角形图元的绘制_第5张图片

  • 位置数据被储存为32位(4字节)浮点值。
  • 每个位置包含3个这样的值。
  • 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
  • 数据中第一个值在缓冲开始的位置。
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*0));
glEnableVertexAttribArray(0);//启用顶点属性;默认禁用

void glVertexAttribPointer(GLuint index,GLint size,GLenum type,GLboolean normalized,GLsizei stride,const GLvoid* pointer);

index: 指定当前配置的顶点属性;与下面顶点着色器中的使用的 layout(location = 0)定义的position顶点属性的位置值相对应。

size: 指定顶点属性的大小。(vec3,由3个值组成,所以是 3)

type: 指定数据类型

normalized:定义数据是否需要被标准化

stride:顶点属性之间的间隔 ( 3 * sizeof(float):三个float 大小)

pointer:数据的起始偏移量

4. 编写顶点着色器

顶点着色器是一个必不可少的可编程着色器;下面是一个简单的将顶点位置输入数据直接输出的顶点着色器源码:

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos,1.0);
}

5. 编写片段着色器

片段着色器所作的是计算像素最后的颜色输出,为了简单测试,片元着色器输出一个固定的颜色:

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0,0.5,0.2,1.0);
}

6. 编译着色器

首先需要创建一个着色器对象,然后把着色器源码附加到着色器对象上,最后编译它:

//顶点着色器对象
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader,1,&vertexShaderSource,NULL);
glCompileShader(vertexShader);

//检测编译错误
int success;
char infoLog[512];
glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success);
if(!success)
{
    glGetShaderInfoLog(vertexShader,512,NULL,infoLog);
    std::cout << "compile failed: " << infoLog << std::endl;
}

//片元着色器对象
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader,1,&fragmentShader,NULL);
glCompileShader(fragmentShader);

glGetShaderiv(fragmentShader,GL_COMPILE_STATUS,&success);
if(!success)
{
    glGetShadeInfoLog(fragmentShader,512,infoLog);
    std::cout << "compile failed: " << infoLog << std::endl;
}

7. 添加着色器程序

着色器程序对象是多个着色器合并之后并最终链接完成的版本;如果要使用以上编译的着色器就必须把它们链接为一个着色器程序对象,然后在渲染ui想的时候激活这个着色器的程序,已激活的着色器程序的着色器将在我们发送渲染调用的时候被使用;当链接着色器至一个程序的时候,他会把每个着色器的输出连接到下一个着色器的输入,如果输出和输入不匹配的时候,就得到一个错误;以下创建一个程序对象:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

glAttachShader(shaderProgram,vertexShader);
glAttachShader(shaderProgram,fragmentShader);
glLinkProgram(shaderProgram);

//检测链接着色器程序是否失败
glGetProgramiv(shaderProgram,GL_LINK_STATUS,&success);
if(!success)
{
    glGetProgramInfo(shaderProgram,512,NULL,infoLog);
    ...
}

8. 激活着色器程序对象

glUseProgram(shaderProgram);

9.绘制

glDrawArrays(GL_TRIANGLES, 0, 6);

10. 删除着色器对象

glDeleteShader(vertexShader);

glDeletaShader(fragmentShader);

源码地址:https://github.com/shadowcpp/OpenGL/blob/master/LearnOpenGL/src/HelloTriangle/HelloTriangle.cpp

使用顶点索引绘制三角形

1. 准备顶点数据

相比于非索引方式下的顶点数据,索引绘制方式下只是储存了不同的顶点,减少了顶点数据的开销。

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, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

2. 创建索引缓冲与输入索引数据

unsigned int EBO;
glGenBuffers(1,&EBO);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW):

3. 绘制

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

void glDrawElements( GLenum mode, GLsizei count,GLenum type, const GLvoid *indices);

mode:指定绘制图元的类型,它应该是下列值之一,GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_QUAD_STRIP, GL_QUADS, and GL_POLYGON.

count:为以mode类型连接的顶点的总数,且根据不同的mode,count小于或等于单个mode类型图元的顶点数*图元数

type:  为索引值的类型,只能是下列值之一:GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT。

indices:元素数组缓存中的偏移地址。

其他与非索引方式相同

源码地址:https://github.com/shadowcpp/OpenGL/blob/master/LearnOpenGL/src/HelloTriangle2/HelloTriangle.cpp

 

以上参考:

  • https://learnopengl.com/
  • OpenGL编程指南

 

你可能感兴趣的:(OpenGL)