LearnOpenGL学习笔记(二) - 着色器与纹理
本文最后更新于 2024年7月15日 晚上
喘口气。
着色器
GLSL
典型着色器程序的结构如下:
1 |
|
对于Vertex Shader,输入变量被称为Vertex Attribute。在OpenGL中一般至少能声明16个Vertex Attribute,每个含4个分量。
向量
向量是GLSL中最常用的数据类型。包含vecn
、bvecn
、ivecn
、uvecn
、dvecn
。前缀代表分量的基本类型,后缀n代表维度数。一般使用vecn
。
使用.x
、.y
、.z
和.w
来获取它们的第1、2、3、4个分量。GLSL也允许对颜色使用rgba
,或是对纹理坐标使用stpq
访问相同的分量。
与CG类似,可以通过重组(Swizzling)的方式填充向量分量。
1 |
|
与此同时,还可以使用向量构造函数直接给向量变量复制,如vec2 vect = vec2(0.5, 0.7)
输入输出
in
和out
关键字用于定义着色器的输入和输出。只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去(前一阶段的输出变量的变量名,应当与后阶段的输入变量的变量名相同)。但在顶点和片段着色器中会有点不同。
对于Vertex Shader,其特殊点在于location
关键字。location
定义了着色器从顶点数据的哪一部分接收数据。例如,在Vertex Shader中,我定义了两个vec4
,第一个是位置数据aPos,它的location
是0;第二个是颜色数据aCol
,它的location
是1。
使用layout (location = 0)
定义某输入变量的location
。
对于Fragment Shader,应当始终保证存在一个vec4
型输出变量,用于输出最终颜色。
Uniform
Uniform
用于在cpp程序中向着色器输入数据,改变其表现。
如其名,Uniform
在每个着色器程序中都是独一无二的。在这个Program链接的所有Shader中,只能存在一个相同名称的Uniform
。
使用uniform
关键字定义Uniform
变量。
1 |
|
在cpp程序中改变uniform
的代码如下:
1 |
|
更新uniform值时,必须Use它所在的Program,否则更新无效。
除glUniform4f外,还有glUniform3i(ivec3)、glUniformfv(float[]或vecn)、glUniformui(unsigned int)等。
多顶点属性
考虑如下顶点数据:
1 |
|
我们知道,可以使用layout(location = x)
来标记不同的顶点属性,通过gVertexAttribPointer
来告诉程序该如何处理这些不同的属性。
1 |
|
自定义着色器类
1 |
|
Shader对象的构造必须要在加载完GLAD proc以后,否则报错。
纹理
简介
纹理(Texture)用于在不增加顶点数量的情况下添加物体的细节。它就像一层贴纸一样贴在几何体上。
纹理也可以用来存储数据。
纹理“贴”到几何体上的过程被称为映射(Map)。纹理坐标(Texture Coordinate)用于指定某个顶点该从纹理的哪个位置采样(Sample,采集Fragment颜色)。非顶点的几何体区域通过片段插值(Fragment Interpolation)采样。
纹理坐标以左下角为原点,右上角为(1, 1)点。
对于超出[0, 1]范围的纹理坐标,可以指定纹理环绕方式来处理。
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。(用于二方连续纹理) |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。(Unity中默认模式) |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
使用glTexParameterx
函数对特定坐标轴设置环绕方式。其中的‘x’代表数据类型,如i(int)、fv(float[])等。
1 |
|
选择GL_CLAMP_TO_BORDER时,使用float数组传入颜色数据。
1 |
|
纹理过滤
纹理坐标可以是任意精度的浮点值,但纹理本身的分辨率却是有限的。因此,OpenGL需要知道,当指定一个纹理坐标时,该如何采样这个点上的像素。最为常见的是GL_NEAREST
和GL_LINEAR
。前者选择最接近坐标的哪个像素,后者会基于坐标附近的像素,计算出插值。像素中心离坐标越近,它对最终颜色的贡献就越大。
二者的主要区别在于,前者会显得更“锯齿”,后者会显得更“模糊”。
当缩放几何体的时候,常常需要对纹理过滤进行设置,通常的做法是在纹理被缩小的时候使用邻近过滤,被放大时使用线性过滤。因为纹理缩小时,纹理像素也会变小,在视觉上,它的“分辨率似乎提高了”。因此,此时采用NEAREST过滤,纹理看起来就不会那么“锯齿化”。纹理放大时,像素看起来会更”明显“,所以使用LINEAR方法让纹理像素不那么明显,过渡更加平滑。
使用glTexParameterx
函数为放大和缩小操作指定过滤方式。
1 |
|
多级渐远纹理
在我们当前的理解中,无论物体远近,其被映射的纹理的分辨率是不变的。对于非常远的物体,它们只会产生很少的Fragment。而OpenGL需要在如此高分辨率的纹理上拾取区区几个像素,是非常困难,并且效果不好的。为了解决这一问题,OpenGL引入了多级渐远纹理(Mipmap)。
Mipmap中,每个纹理的大小是前一个纹理的二分之一。当物体与相机的距离超过一定阈值后,OpenGL会采用小一级的纹理进行采样。
通过glGenerateMipmap
函数创建Mipmap。
与纹理过滤选项类似,OpenGL提供了多种Mipmap匹配选项,用于缓解阈值附近Mipmap切换突兀的问题。
过滤方式 | 描述 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
使用glTexParameteri
设置Mipmap过滤方式。
1 |
|
一般只会对MIN_FILTER选项使用Mipmap。对MAG_FILTER使用Mipmap是不正确的。
加载与创建
头文件stb_image.h
中的stbi_load
函数接收图片路径作为输入,将图片的宽度、高度和颜色通道数输出到三个int变量上。
1 |
|
与glGenBuffers
类似,使用glGenTextures(int cnt, unsigned int* addr)
生成纹理对象并获取句柄。
使用glBindTexture(GL_TEXTURE_2D, unsigned int texture)
绑定纹理对象句柄与上下文目标。
使用glTexImage2d
将图片信息复制到上下文目标中。
1 |
|
与glBufferData
类似,glTexImage2d
的作用就是让当前绑定的纹理对象附加上真正的纹理图像。
glGenerateMipmap(GL_TEXTURE_2D)
让OpenGL自动为我们生成、配置Mipmap,无需手动配置。
完成纹理和Mipmap生成后,应当释放图片内存:stbi_image_free(data);
完整的生成纹理过程如下:
1 |
|
纹理单元
sample2d
类型uniform无需使用glUniform
赋值。但glUniform
可以设置sampler2d
的位置值,这样我们就能给着色器设置多个纹理。一个纹理的位置值称为一个纹理单元(Texture Unit),其默认值为0。
使用glActiveTexture(GL_TEXTUREX)
激活X号纹理单元, 随后使用glBindTexture
为该位置值的纹理单元绑定纹理对象。
1 |
|
使用自定义着色器类的setInt
函数设置纹理单元位置值。
1 |
|
在OpenGL中,纹理坐标的原点在左下角。而多数图像文件格式如PNG、JPEG等原点在左上角。所以直接加载这类图片会导致图片上下颠倒。
使用stbi_set_flip_vertically_on_load(true)
反转图片y轴。
完整代码
1 |
|
1 |
|
1 |
|