OpenGL入门笔记(2)

一、图形渲染管线:

1、图形渲染管线:

OpenGL的大部分工作:把3D坐标转变为适应你屏幕的2D像素。

功能:指原始图形数据经过期间各种变化最后出现在屏幕上。

分类:①3D坐标→2D坐标;②2D坐标→实际有颜色像素。

【注】:2D坐标≠像素,2D坐标——一个点在2D空间中的位置。2D像素——这个点的近似值,受屏幕/窗口分辨率的限制。

阶段:(蓝色代表可以自定义着色器)

OpenGL的着色器由OpenGL着色器语言(GLSL)写成。

工作大致流程介绍(以上图所示为例):

顶点着色器:顶点数据(3个3D坐标和颜色值(顶点属性))传送到定点着色器,将3D坐标转为另一种3D坐标,并可以对顶点属性做一些基本处理。

几何着色器:将选择性输入的一组顶点形成图元,并发出新的顶点形成新的图元生成形状。(上图生成两个三角形)。

形状(图元)装配:将顶点着色器(或几何着色器)输出的所有点输入,并装配成指定的图元形状。(上图生成两个三角形)。

光栅化:把图元映射成屏幕上相应的像素,生成供片段着色器使用的片段。

裁剪:丢弃超出视图之外的所有像素,提高执行效率。

片段着色器(OpenGL高级效果产地):计算一个像素的最终颜色,片段着色器包含3D场景数据,可被用来计算最终像素颜色。

Alpha测试和混合阶段:检测片段对应深度(和模板)值,判断这个像素是其他物体前还是后,是保留还是丢弃。检查Alpha值(一个物体透明度)并对物体进行混合。

【注】现代OpenGL必须定义至少一个顶点着色器和一个片段着色器。

2、顶点的输入:

【注】:顶点着色器处理后的顶点坐标(标准化设备坐标NDC)需要满足的条件:①3D坐标,②x, y, z 轴在范围-1.0到1.0上。

//标准化设备坐标
//得到2D,只需要把深度值设为0,及z轴为0
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数据发送显卡速度较慢,大批发送可以在发送完毕后顶点着色器立即访问顶点)。它是OpenGL的对象,使用glGenBuffers函数生成:

//生成一个带有缓冲ID的VBO对象
unsigned int VBO;
glGenBuffers(1,&VBO);

不同缓冲类型可以同时绑定,顶点缓冲对象的缓冲类型:GL_ARRAY_BUFFER,使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上。

glBindBuffer(GL_ARRAY_BUFFER,VBO);
//此刻起,使用的任何GL_ARRAY_BUFFER目标上的缓冲调用都会用来配置当前绑定的VBO

使用glBufferData函数把定义的顶点数据复制到缓冲内存中

//将定义的数据复制到绑定的缓冲中
//第一个参数:目标缓冲类型
//第二个参数:传输数据的大小(以字节为单位),用sizeof计算顶点数据的大小即可
//第三个参数:希望发送的实际数据
//第四个参数:指定我们希望显卡如何管理给定的数据
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);

管理给定数据有三种形式:

① GL_STATIC_DRAW:数据不会或几乎不会改变。

② GL_DYNAMIC_DRAW:数据会被改变很多。

③ GL_STREAM_DRAW:数据每次绘制时都会改变。

3、顶点着色器:

简易顶点着色器编写(GLSL语言):

//代表GLSL版本3.3,与OpenGL一致
#version 330 core
//因为只需要位置数据,且是3D,所以用 in vec3 aPos
//layout(location = 0)设定输入变量的位置值
layout(location = 0) in vec3 aPos;

void main()
{
    //GLSL的一个向量有4个分量,vec.x, vec.y, vec.z, vec.w, 最后一个代表透视除法
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

 将上述代码硬编码在代码文件顶部的C风格字符串中:

const char *vertexShaderSource = "#version 330 code\n"
    "layout(location = 0) in vec3 aPos;\n "
    "void main()\n"
    "{\n"
    "    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0"

 让OpenGL使用它并编译:

//创建着色器对象
unsigned int vertexShader;
//把创建的着色器类型以参数GL_VERTEX_SHADER形式传递给glCreateShader
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<<"ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"<<infoLog<<std::endl;
}

  4、片段着色器:

简易片段着色器编写(GLSL语言)

//计算像素最后的颜色输出
#version 330 core
out vec4 FragColor;

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

   让OpenGL使用它并编译:

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource,NULL);
glCompileShader(fragmentShader);

 5、着色器程序:

着色器程序原理 :若要使用创建的着色器,需要将编译的着色器链接成着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活的会在发送渲染调用时使用。

创建一个程序对象shaderProgram:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

链接顶点着色器和片元着色器:

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

判断链接着色器程序是否成功:

int success;
char infoLog[512];

glGetProgramiv(shaderProgram,GL_LINK_STATUS,&success);
if(!success)
{
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::Program::LINK_FAILED\n" << infoLog << std::endl;
}

激活程序对象:

glUseProgram(shaderProgram);

删除顶点着色器和片段着色器:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

 6、链接顶点属性:

 指定在渲染前OpenGL如何解释顶点数据:

//第一个参数:指定我们要配置的顶点属性,在前面layout(location = 0)定义position顶点属性的位置值为0,这里也为0
//第二个参数:指定顶点属性的大小,顶点属性为vec3,由3个值组成
//第三个参数:指定数据类型,用的浮点数
//第四个参数:是否希望数据被标准化
//第五个参数:步长,也就是在连续顶点属性组之间的间隔,这里是3个float,且没有间隔,所以用这个,可以用0,这时候让OpenGL自己决定(前提数值是紧密排列才可用)
//第六个参数:表示位置数据在缓冲中起始位置的偏移量,由于位置数据在数组的开头,所以用0
glVertexAttribPointer(0,3,GL_FALSE,3*sizeof(float),(void*)0);

//以顶点属性位置作为参数,启用顶点属性,顶点属性默认禁用
glEnableVertexAttribuArray(0);

在OpenGL上绘制一个物体

//复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);

//设置顶点属性指针:
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);

//当我们渲染一个物体时要使用着色器程序:
glUserProgram(shaderProgram);

//绘制物体
someOpenGLFunctionThatDrawOurTriangle();

7、顶点数组对象(VAO):

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

①glEnableVertexAttribArray和glDisableVertexAttribArray的调用

②通过glVertexAttribPointer设置顶点属性配置

③通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象

创建一个VAO和创建一个VBO:

unsigned int VAO;
glGenVertexArrays(1,&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);
glEnableVertexAttrbArray(0);

[...]

//...绘制代码(渲染循环中)...
//4、绘制物体
glUserProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawTriangle();

绘制物体glDrawArrays函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元:

glUserProgram(shaderProgram);
glBindVertexArray(VAO);
//第一个参数:绘制三角形
//第二个参数:顶点数组的起始索引
//第三个参数:最后绘制多少个顶点
glDrawArrays(GL_TRIANGLE,0,3);

  8.元素缓冲对象:

 绘制矩形:

//列出所有的点
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,2,3)就是顶点数组vertices的下标
    //这样可以由下标代表顶点组合成矩形

    0,1,2,//第一个三角形
    1,2,3  //第二个三角形


};

创建元素缓冲对象

unsigned int EBO;
glGenBuffers(1,&EBO)

 用glBufferData把索引复制到缓冲里

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

从索引缓冲区渲染三角形

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
//第一个参数:绘制模式
//第二个参数:绘制6个顶点
//第三个参数:索引的类型
//第四个参数:EBO的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们在这会填写0
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);

   绘制代码:

// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

[...]

// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 
6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);

 呈现结果:

                                                                                                     

二、着色器:

定义:着色器只是一种把输入转化为输出的程序,着色器也是一种非常独立的程序,因为它们之间不能相互通信,它们之间唯一的沟通只有通过输入和输出。

1、GLSL(着色器语言,类C):

着色器典型结构:

//版本号
#version version_number

//输入
in type in_variable_name;
in type in_variable_name;

//输出
out type out_variable_name;

//uniform函数
uniform type uniform_name;

//main()函数
void main()
{
    //处理输入并进行一些图形操作
    ...
    //输出处理过的结果到输出变量
    out_variable_name = weird_stuff_we_processed;
}

用GL_MAX_VERTEX_ATTRIBS查询顶点属性                                                              

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS,&nrAttributes);
std::cout<<"Maximum nr of vertex attributes supported: "<< nrAttributes << std::endl;
//通常情况下至少返回16个,大部分情况下是够用的了

【注】顶点属性的声明是有上限的,取决于硬件,OpenGL至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的属性。

2、数据类型

数据类型:int、float、double、uint、bool,Vector和Matrix(两个容器类型)。

向量

GLSL的向量:可以包含2,3或4个分量容器,分量类型可以是默认基础类型的任意一个。

类型含义
vecn(常用)包含n个float分量的默认向量
bvecn包含n个bool分量的向量
ivecn包含n个int分量的向量
uvecn包含n个unsigned int分量的向量
dvecn包含n个double分量的向量

重组:

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

向量传参:

vec2 vect = vec2(0.5,0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz,1.0);

3、输入和输出:

(1)顶点着色器:顶点数据直接接收输入。

使用location这一元数据指定输入变量,这样可以在CPU上配置顶点属性。而且需要为它输入提供一个额外的layout标识,也可以忽略layout(location = 0)标识符,用glGetAttribLocation查询。

#version 330 core
layout(location = 0) in vec3 aPos;//位置变量的属性位置值为0

out vec4 vertexColor;//为片段着色器指定一个颜色输出

void main()
{
    gl_Position = vec4(aPos,1.0) //注意我们如何把一个vec3作为vec4的构造器的参数
    vertexColor = vec4(0.5,0.0,0.0,1.0); //把输出变量设置为红色
}

(2)片段着色器:生成一个最终输出的颜色。

若没有定义输出颜色,默认会渲染为黑色或白色。

#version 330 core
out vec4 FragColor;

in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

void main()
{
    FragColor = vertexColor;
}

4、Uniform:

从应用程序在CPU上传递数据到GPU上的着色器方式,但与顶点属性不同。

特点:

①uniform是全局的,意味着uniform变量必须在每个着色器程序中是独一无二的,而且可以被着色器程序在任意着色器在任意阶段访问。

②无论你把uniform值设成什么,uniform会一直保存它们的数据,直到被重置或更新。

uniform设置三角形颜色:

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor; //在OpenGL程序代码中设定这个变量

void main()
{
    FragColor = ourColor;
}

【注】若声明的uniform 在GLSL 中未用,编译器会静默移除这个变量,可能会导致几个非常麻烦的错误!!!

随时间改变颜色

//获取运行秒数
float timeValue = glfwGetTime();
//用sin函数让颜色在0.0到1.0之间改变,最后将结果储存在greenValue里
float greenValue = (sin(timeValue)/2.0f)+0.5f;
//查询uniform ourColor位置值,返回-1就代表没有
int vertexColorLocation = glGetUniformLocation(shaderProgram,"ourColor");
glUseProgram(shaderProgram);
//设置uniform值
glUniform4f(vertexColorLocation,0.0f,greenValue,0.0f,1.0f);

//注意:
//查询uniform地址不需要你之前使用过着色器程序,但是更新一个uniform之前必须先使用程序,因为它是在当前激活的着色器中设置的。

glUniform的后缀,标识设定的uniform类型。

后缀含义
f函数需要一个float作为它的值
i函数需要一个int作为它的值
ui函数需要一个unsigned int作为它的值
3f函数需要3个float作为它的值
fv函数需要一个float向量/数组作为它的值

颜色慢慢变化

while(!glfwWindowShouldClose(window))
{
    //输入
    processInput(window);

    //渲染
    //清除颜色缓冲
    glClearColor(0.2f,0.3f,0.3f,1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    
    //记得激活着色器
    glUseProgram(shaderProgram);
    
    //更新uniform颜色
    float timeValue = glfwTime();
    float greenValue = sin(timeValue)/2.0f + 0.5f;
    int vertexColorLocation = glGetUniformLocation(shaderProgram,"ourColor");

    //绘制三角形
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES,0,3);

    //交换缓冲并查询IO事件
    glfwSwapBuffers(window);
    glfwPollEvents();
}

5、顶点和颜色属性:

定义顶点和颜色:

float vertices[] = {
    //位置                //颜色
    0.5f , -0.5f , 0.0f,  1.0f, 0.0f, 0.0f,   //右下
    -0.5f, -0.5f , 0.0f,  0.0f, 1.0f, 0.0f,   //左下
    0.0f ,  0.5f , 0.0f,  0.0f, 0.0f, 1.0f    //顶部
}

调整顶点着色器:

#version 330 core
//位置变量的属性位置值为0
layout(location = 0) in vec3 aPos;
//颜色变量的属性位置值为1
layout(location = 1) in vec3 aColor;

//向片段着色器输出一个颜色
out vec3 ourColor;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    
    //将ourColor设置为我们从顶点数据那里得到的输入颜色
    ourColor = aColor;
}

调整片段着色器:

#version 330 core
out vec4 FragColor;
in vec3 ourColor;

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

由于添加了另一个顶点属性,并更新了VBO的内存,就需要配置顶点属性指针:

//位置属性,位置属性在前,偏移量为0
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);


//颜色属性,位置属性在位置数据之后,偏移量为3*sizeof(float)
glVertexAttribPointer(1,3,GL_FlOAT,GL_FALSE,6*sizeof(float),(void*)(3*sizeof(float));
glEnableVertexAttribArray(1);

【注】呈现结果会出现片段插值现象,是由于光栅化阶段通常会造成比原指定顶点更多的片段。

6、把着色器放在头文件里:

#ifndef SHADER_H
#define SHADER_H

#include<glad/glad.h>; // 包含glad类来获取所有的必须OpenGL头文件

#include<string>
#include<fstream>
#include<sstream>
#include<iostream>

class Shader
{
public:
    //程序ID
    unsigned int ID;

    //构建器读取并构建着色器
    Shader(const char* vertexPath, const char* fragmentPath);

    //使用/激活程序
    void use();

    //uniform工具函数
    //可以查询位置值,并设置它的值
    void setBool(const std::string &name, bool value) const;
    void setInt(const std::string &name, int value) const;
    void setFloat(const std::string &name, float value) const;
}

#endif

7、写一个完整的着色器类:

①使用C++文件流读取着色器内容,储存到几个string对象里:

Shader(const char* vertexPath, const char* fragmentPath)
{
    //1、文件路径中获取顶点/片段着色器
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;

    //保证ifStream对象可以抛出异常:
    vShaderFile.exception(std::ifstream::failbit | std::ifstream::badbit);
    fShaderFile.exception(std::ifstream::failbit | std::ifstream::badbit);

    try{
        //打开文件
        vShaderFile.open(vertexPath);
        fShaderFile.open(vertexPath);

        //读取文件的缓冲内容到数据流中
        vShaderStream<< vShaderFile.rdbuf();
        fShaderStream<< fShaderFile.rdbuf();

        //关闭文件处理器
        vShaderFile.close();
        fShaderFile.close();

        //转换数据流到string
        vertexCode = vShaderStream.str();
        fragmentCode = fShaderStream.str();
    
    }
    catch(std::ifstream::failure e)
    {
        std::cout<<"ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ"<<std::endl;
    
    }

    const char* vShaderCode = vertexCode.c_str();
    const char* fShaderCode = vertexCode.c_str();
    [...]

}

②编译和链接着色器

//2、编译着着色器
unsigned int vertex, fragment;
int success;
char infoLog[512];

//顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex,1,&vShaderCode,NULL);
glCompileShader(vertex);

//打印编译错误(如果有的话)
glGetShaderiv(vertex,GL_COMPILE_STATUS,&success);
if(!success)
{
    glGetShaderInfoLog(vertex,512,NULL,infoLog);
    std::cout<<"ERROR::SHADER::VERTEX::COMPILATION_FAILURE\n"<<infoLog<<std::endl;
}

//片段着色器也类似
[...]

//着色器程序
ID = glCreateProgram();
glAttachShader(ID,vertex);
glAttachShader(ID,fragment);
glLinkProgram(ID);

//打印连接错误(如果有的话)
glGetProgramiv(ID,GL_LINK_STATUS,&success);
if(!success)
{
    glGetProgramInfoLog(ID,512,NULL,infoLog);
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILURE\n"<<infoLog<<std::endl;
}
//删除着色器,它们已经链接到我们的程序中,已经不再需要
glDeleteShader(vertex);
glDeleteShader(fragment);

③use函数

void use()
{
    glUseProgram(ID);
}

④uniform的setter函数

void setBool(const std::string &name, bool value) const
{
    glUniform1i(glGetUniformLocation(ID,name.c_str()),(int)value);
}

void setInt(const std::string &name, int value) const
{
    glUniform1i(glGetUniformLocation(ID,name.c_str()),value);
}
void setFloat(const std::string &name, float value) const
{
    glUniform1f(glGetUniformLocation(ID,name.c_str()),value);
}

使用上述着色器:

Shader ourShader("path/to/shaders/shader.vs","path/to/shader.fs");
...
while(...)
{
    ourShader.use();
    ourShader.setFloat("someUniform",1.0f);
    DrawStuff();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值