分享

OpenGL学习笔记(4)

 QomoIT 2019-07-19

引言

上一次已经通过着色器绘制了一个2D的三角形,这次笔记记录了教程从着色器到变换的内容,变成了一个有纹理,会移动的2D三角形。

着色器

这一节主要是将着色器中的数据类型和uniform属性。

GLSL中的数据类型

GLSL中的基本数据类型与C语言中差不多,有int,float,double,uint,bool。除此之外还有2个容器类型,向量vec和矩阵mat。
GLSL中的vec使用十分灵活,可以用.x,.y,.z,.w来获取一个vec的第1,2,3,4个分量,或者rgba(颜色),stpq(纹理)来获取。GLSL允许你像下面一样使用vec:

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);

着色器之间传递数据

OpenGL允许位于管程前面的着色器像后面的着色器传递数据。
例如在vertexShader中输出一个vertexColor,使用out关键字

out vec3 VertexColor;

在fragmentShader中获取vertexColor,使用关键字in

in vec3 VertexColor;

名字和类型一定要对,如果后面的in前面没有对应的out,编译和链接都不会出错,但是in的值会被设为0。

着色器中的Uniform属性

这个比较重要,Uniform属性是你的OpenGL程序向着色器程序输入数据的另一个途径。
第一个途径是通过上一次笔记讲的VertexAttrib(顶点属性),一般每一个顶点独有的固定的属性会使用这个传递。
第二个途径就是Uniform变量,它在每个着色器程序对象中都是独一无二的,可以被着色器程序的任意着色器在任意阶段访问,而且数据对于每一个顶点不能被改变。Uniform比较适合用来输入根据当前程序状态才能被确定,或是多个顶点的一样属性。
在着色器中定义uniform变量

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

在OpenGL程序中设定变量

// 首先获取uniform变量在着色器程序中的位置
int uniformLocation = glGetUniformLocation(shaderProgram, "ourColor");
// 设置Uniform变量
glUniform4f(uniformLocation, 0.0f, greenValue, 0.0f, 1.0f);

glUniform后面可用的后缀及其含义:

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

着色器程序类

不能总是在主函数储存着色器的代码,需要将着色器的代码存在单独的文件里,用的时候从文件读取。着色器文件读取,编译链接都是一个相对比较流程化的过程,可以统一到一个Shader类中(其实是ShaderProgram类)。跟着教程写出的着色器类Shader.hShader.cpp

纹理

纹理的绘制要求每个顶点有纹理坐标属性,2D纹理坐标左下角是(0,0),右上角是(1,1)如下:
在这里插入图片描述

纹理的环绕方式

OpenGL里的纹理有四种环绕方式

环绕方式 描述
GL_REPEAT 对纹理的默认行为。重复纹理图像。
GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘。
GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。

效果如下
在这里插入图片描述
使用glTexParameter*函数对单独的一个坐标轴设置(s、t(如果是使用3D纹理那么还有一个r)它们和x、y、z是等价的):

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

纹理过滤

纹理过滤方式就是如何由连续的纹理坐标离散的纹理像素确定当前顶点的颜色。
GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
在这里插入图片描述
GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
在这里插入图片描述
两种效果的比较,Nearest颗粒感较重,Linear比较模糊。
在这里插入图片描述

还有一种叫做多级渐远纹理的技术,是为了解决远处物体使用高分辨率的纹理所造成的内存浪费和不真实感。其实就是生成一系列不同大小、分辨率的原图缩小版,生成多级渐远纹理可以调用glGenerateMipmaps函数
在这里插入图片描述
然后根据距离选择不同分辨率的图片作为纹理,这个部分是OpenGL帮你做的,你需要的只是选择OpenGL在切换两个不同级别的多级渐远纹理层之间时的过滤方式

过滤方式 描述
GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
GL_LINEAR_MIPMAP_LINEAR 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样

前面的值代表纹理过滤的方式,后面的值代表多级渐远纹理的方式
下面是设置过滤方式的示例代码:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

注意只有在min的时候设置多级渐远才有效果,因为多级渐远时纹理图片只会缩小,不会放大。

加载纹理

stb_image.h是Sean Barrett的一个非常流行的单头文件图像加载库,它能够加载大部分流行的文件格式,并且能够很简单得整合到你的工程之中。引入了stb_image库之后,加载一个纹理过程应该如下:

unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);   
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data){
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
}
else{
    std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);

在实际的应用中,会把上述过程封装成一个函数

unsigned int TextureFromFile(const char *path){
    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0);
    if(data){
        GLenum format;
        if(nrComponents == 1)
            format = GL_RED;
        else if(nrComponents == 3)
            format = GL_RGB;
        else if(nrComponents == 4)
            format = GL_RGBA;
        
        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }else{
        std::cout << "Texture failed to load at path: " << path << std::endl;
        stbi_image_free(data);
    }

    return textureID;
}

纹理的应用

为了将纹理传递到着色器中,GLSL提供了一个纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,sampler2D。可以在着色器中这样声明一个着色器。

uniform sampler2D ourTexture;

因为大小问题(应该吧),纹理类型不能像vec一样直接使用glGetUniformLocation+glUniform设置。需要首先设置着色器里的Uniform sampler的纹理单元(只需在生成着色器程序后设置一次)

ourShader.use();
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 将texture1的纹理单元设置为0
glUniform1i(glGetUniformLocation(ourShader.ID, "texture2"), 1); // 将texture2的纹理单元设置为1

紧接着在需要渲染的时候分别对纹理单元绑定纹理

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);

传到着色器中之后,需要通过GLSL内置的texture函数使用纹理。

FragColor = texture(ourTexture, TexCoord); // TexCoord是一个vec2,代表纹理坐标

变换

OpenGL里坐标的变换是通过坐标向量和矩阵相乘实现的。目前常用的矩阵由三种操作:缩放,位移,旋转

坐标向量

目前用到的坐标向量一般都是一个四元向量,前三个分别是x,y,z的坐标,最后一个值称为齐次坐标。
\begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix}

齐次坐标(Homogeneous Coordinates)

向量的w分量也叫齐次坐标。想要从齐次向量得到3D向量,我们可以把x、y和z坐标分别除以w坐标。我们通常不会注意这个问题,因为w分量通常是1.0。使用齐次坐标有几点好处:它允许我们在3D向量上进行位移(如果没有w分量我们是不能位移向量的),而且下一章我们会用w值创建3D视觉效果。

如果一个向量的齐次坐标是0,这个坐标就是方向向量(Direction Vector),因为w坐标是0,这个向量就不能位移(译注:这也就是我们说的不能位移一个方向)。

缩放矩阵

\begin{bmatrix} \color{red}{S_1} & \color{red}0 & \color{red}0 & \color{red}0 \\ \color{green}0 & \color{green}{S_2} & \color{green}0 & \color{green}0 \\ \color{blue}0 & \color{blue}0 & \color{blue}{S_3} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{S_1} \cdot x \\ \color{green}{S_2} \cdot y \\ \color{blue}{S_3} \cdot z \\ 1 \end{pmatrix}

位移矩阵

\begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}{T_x} \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}{T_y} \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}{T_z} \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x + \color{red}{T_x} \\ y + \color{green}{T_y} \\ z + \color{blue}{T_z} \\ 1 \end{pmatrix}

旋转矩阵

沿x轴旋转:
\begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}0 \\ \color{green}0 & \color{green}{\cos \theta} & - \color{green}{\sin \theta} & \color{green}0 \\ \color{blue}0 & \color{blue}{\sin \theta} & \color{blue}{\cos \theta} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x \\ \color{green}{\cos \theta} \cdot y - \color{green}{\sin \theta} \cdot z \\ \color{blue}{\sin \theta} \cdot y + \color{blue}{\cos \theta} \cdot z \\ 1 \end{pmatrix}
沿y轴旋转:
\begin{bmatrix} \color{red}{\cos \theta} & \color{red}0 & \color{red}{\sin \theta} & \color{red}0 \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}0 \\ - \color{blue}{\sin \theta} & \color{blue}0 & \color{blue}{\cos \theta} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{\cos \theta} \cdot x + \color{red}{\sin \theta} \cdot z \\ y \\ - \color{blue}{\sin \theta} \cdot x + \color{blue}{\cos \theta} \cdot z \\ 1 \end{pmatrix}
沿z轴旋转:
\begin{bmatrix} \color{red}{\cos \theta} & - \color{red}{\sin \theta} & \color{red}0 & \color{red}0 \\ \color{green}{\sin \theta} & \color{green}{\cos \theta} & \color{green}0 & \color{green}0 \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{\cos \theta} \cdot x - \color{red}{\sin \theta} \cdot y \\ \color{green}{\sin \theta} \cdot x + \color{green}{\cos \theta} \cdot y \\ z \\ 1 \end{pmatrix}

矩阵组合的顺序

作者建议先缩放,再旋转,最后位移。即矩阵的乘法最好是下面的顺序
Trans . Rotat . Scale . Vec

GLM

GLM是OpenGL Mathematics的缩写,它是一个只有头文件的库,是专门为OpenGL量身定做的数学库。
我们需要的GLM的大多数功能都可以从下面这3个头文件中找到:

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

GLM中有mat, vec类型,以及许多方便的操作,比如生成三种变换矩阵

glm::mat4 trans = glm::mat4(1.0f);
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5)); 

将矩阵通过uniform送入顶点着色器,并且在顶点着色器中将矩阵与顶点坐标相乘就完成了位置的变换

unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));

本文的思路和出现的图来自于 learnopengl-cn.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多