Fork me on GitHub

三维拾取

原理

​ 让每一个像素都有自己独特的索引,当鼠标点击时获取鼠标点击处的像素和像素索引(一般glReadPixels函数是获取鼠标点击处的指定的数据,比如说3d拾取就选择获取rgb颜色数据 ,但是此处并不是真正的获取颜色数据,而是在颜色纹理中填充的是三元索引,所以从颜色纹理中获取的是三元索引),就可知道点击的像素属于哪个对象 ,就可以知道点击到了哪个对象。

​ 通过创建帧缓冲, 将三元索引代替每个顶点像素的颜色rgb存储进入帧缓冲的颜色缓冲,从而使像素位置和索引相对应。这样在点击的时候就可以知道点击到了哪个索引。

三元索引:

  1. 第一级是像素所在物体的索引值,场景中的每一个物体都会得到一个唯一的索引;
  2. 物体的draw call的索引,这个索引会在开始渲染新物体时重置;
  3. 每个draw call中图元的索引值,每次新的draw call开始时该索引会重置;

拾取阶段

把模型相应数据传进拾取着色器,绘制一遍模型,把相应的顶点和索引绘制进帧缓冲。

step1:创建帧缓冲,绑定颜色和深度纹理

把模型的各个顶点放入帧缓冲的深度纹理,把模型的对象索引,绘制索引,原始索引放入帧缓冲的颜色纹理。下面只显示了基于阴影贴图init代码(只用了深度纹理)后添加的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Init()
{
//注意颜色纹理的参数设置GL_RGB32F、GL_RGB
glGenTextures(1, &pickingTexture);
glBindTexture(GL_TEXTURE_2D, pickingTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, 1024, 1024,0, GL_RGB, GL_FLOAT, NULL);
//将此纹理附加到FBO的GL_COLOR_ATTACHMENT0目标,这将使其成为片段着色器输出的目标。
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,pickingTexture, 0);

glDrawBuffer(GL_COLOR_ATTACHMENT0);
glBindTexture(GL_TEXTURE_2D, 0);
//重置读取缓冲区和帧缓冲区
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

}
step2:拾取着色器

顶点着色器进行正常的MVP变换,片段着色器代码如下:

​ 在同一次绘图调用中对象索引和绘图索引对于所有像素都是相同的,因此它们来自统一的变量。为了获得原始索引,使用内置变量gl_PimitiveID(渲染过程中的图元数量)。这是系统自动维护的原语的运行索引。系统在绘图开始时将gl_PimitiveID重置为零。这使得很难区分“背景”像素和实际被对象覆盖的像素。为了克服这个问题,在将索引写入输出之前先将其递增一。这意味着可以识别背景像素,因为它们的图元ID为零,而对象覆盖的像素具有1…n作为图元ID。

1
2
3
4
5
6
7
8
#version 330
uniform uint gDrawIndex;
uniform uint gObjectIndex;
out vec3 FragColor;
void main()
{
FragColor = vec3(float(gObjectIndex), float(gDrawIndex),float(gl_PrimitiveID + 1));
}
step3:写入帧缓冲

主循环中:开启拾取帧缓冲-开启拾取着色器-把模型相应数据传进拾取着色器-绘制一遍模型-恢复默认缓冲

1
2
3
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
/*绘制图形*/
glBindFramebuffer(GL_FRAMEBUFFER, 0);

绘制阶段

如果左键点击了,获取鼠标点击处的像素的三元索引(此处将点击处改成了光标位置处)
如果该像素不是背景,通过得到的该像素的绘制索引和原始索引绘制出相应片段(此处将绘制出相应片段改成了设置不同的颜色)

step1:从点击的像素中获取三元索引

​ 函数参数为鼠标点击的屏幕上的xy坐标。要读取FBO,必须将其绑定到GL_read_FRAMEBUFFER目标。然后需要使用函数glReadBuffer()指定从哪个颜色缓冲区读取,原因是FBO可以包含多个颜色缓冲区。函数glReadPixels执行实际读取,它使用左下角(第一对参数)和宽度/高度(第二对参数)指定的矩形,并将结果读入最后一个参数给出的地址,此处矩形的大小是一个纹素。因为需要原始数据,因此使用GL_RGB作为格式,使用GL_FLOAT作为类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct PixelInfo {
float ObjectID;float DrawID;float PrimID;
PixelInfo() {ObjectID = 0.0f;DrawID = 0.0f;PrimID = 0.0f;}};
PixelInfo ReadPixel(unsigned int x, unsigned int y)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glReadBuffer(GL_COLOR_ATTACHMENT0);
PixelInfo Pixel;
glReadPixels(x, y, 1, 1, GL_RGB, GL_FLOAT, &Pixel);
//必须重置读取缓冲区和帧缓冲区
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
return Pixel;
}
step2:绘制

​ 此处绘制一个物体正方体,但是利用不同的MVP矩阵将其draw两次。两次的gObjectIndex都为0,第一次draw的DrawIndex是0,第二次draw的DrawIndex是1。

1
2
glUniform1ui(gObjectIndexID, 0);
glUniform1ui(gDrawIndexID, 1);

glfwGetCursorPos获取窗口上光标的位置,存入xy中。注意y轴的计算方式:height-int(y)+1(屏幕y轴与世界空间的y轴相反)。通过如下代码:如果光标在第一个物体的第一个三角形,则为红色;第一个物体的第二个三角形,则为墨绿色;如果光标在第二个物体的第一个三角形,则为蓝色;第二个物体的第二个三角形,则为灰色;光标不在物体上则为绿色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    glfwGetCursorPos(window,&x,&y);
PixelInfo Pixel = ReadPixel(int(x), 768-int(y)+1);
if (Pixel.PrimID != 0) {
glm::uint a = (uint)Pixel.DrawID;
glm::uint b = (uint)Pixel.PrimID;
if (a == 0) {
if(b == 1){glColor4f(0.4f, 0.0f, 0.0f, 1.0f);}
if (b == 2) {glColor4f(0.4f, 0.4f, 0.0f, 1.0f);}
}
else if (a == 1) {
if (b == 1) {glColor4f(0.0f, 0.0f, 0.4f, 1.0f);}
if (b == 2) {glColor4f(0.4f, 0.4f, 0.4f, 1.0f);}
}
}
else {
glColor4f(0.0f, 0.4f, 0.0f, 1.0f);
}

真正绘制阶段

主循环中正常绘制,使物体显示在屏幕。

效果:

img image-20230128185721248
image-20230128185707432 image-20230128185729121
image-20230128185734563

实例渲染

假设想渲染一个有一支庞大军队经过的场景,要用一个士兵的模型渲出成千上万的士兵大军。一种方法是每次绘制一个士兵都调用一次draw call,并更新当前士兵相关的一致变量(位置、尺寸等)。例如,每个士兵都处于不同的位置,每个士兵都可以更高或者更矮尺寸不一等。因此,每次draw call都要更新当前士兵模型的WVP矩阵,这样开销会很大。

实例渲染:一个实例就是要渲染的模型在场景的一次实际出现,这里例子中就是一个士兵。实例化渲染意味着可以在一次draw call中渲染多个实例,并为每个实例提供其特有的属性。

方法1:在一个单独的顶点缓冲VB中定义实例的属性(例如:WVP矩阵)。通常顶点处理器会逐顶点每次处理一个顶点数据,有了存放实例数据的顶点缓冲VB后,定点处理器在每次处理一个顶点时要等所有顶点都已经被绘制,存有实例数据的VB提供的属性数据是针对所有顶点公共的。(第一种是将实例数据作为顶点属性传递)

方法2:使用一个内置的叫做gl_InstanceID的shader变量,它是用来告诉当前的实例索引index的。我们可以根据这个索引值来定位一致变量数组中对应的实例数据来进行相应的渲染操作。

image-20230127161506622

这里有一个包含100个顶点的模型,每个顶点都有位置、法线、纹理坐标属性,三个属性中每个属性都有他们自己的顶点缓冲。另外,还有第四个顶点缓冲buffer存放着三个WVP矩阵。先使用第一个WVP矩阵应用到100个顶点pos位置的绘制上,然后再使用第二个WVP矩阵绘制一遍,然后再使用第三个绘制,并且这个过程是在一个draw call中实现的,而不是三个。WVP矩阵会作为输入参数传送到顶点处理器中,但由于第四个VB被标记为存有实例数据,所以WVP矩阵在所有顶点绘制完之前是不会变化的。

cpp文件:

​ 由于WVP矩阵是一个4x4矩阵,不能仅为其启用一个顶点属性,因为顶点属性最多只能包含4个浮点或整数。因此需要启用和配置4个连续顶点属性的循环。每个属性将包含矩阵中的一个向量(第一个属性1包含三个MVP矩阵的第一个向量,第二个属性2包含三个MVP矩阵的第二个向量……)。四个属性中的每一个都由四个浮点组成,一个矩阵中的属性与下一个矩阵之间的距离是4x4矩阵的大小,因此一共占了1、2、3、4顶点属性。

​ glVertexAttribDivisitor()函数使其成为实例数据而不是顶点数据。它采用两个参数:第一个是顶点数组属性,第二个参数告诉OpenGL在实例化渲染过程中属性前进的速度。默认情况下,除数为零。如果除数是10,这意味着前10个实例将使用缓冲区中的第一条数据,接下来的10个实例会使用第二条数据。此处希望每个实例都有一个专用的WVP矩阵,因此使用1的除数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//创建存储了3个MPV矩阵的顶点属性数组,glm::mat4类型
static glm::mat4 WVPMats[] = {MVP1,MVP2,MVP3};
//创建并绑定缓冲区
GLuint MVPbuffer;
glGenBuffers(1, &MVPbuffer);
glBindBuffer(GL_ARRAY_BUFFER, MVPbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::mat4)* 3, WVPMats, GL_DYNAMIC_DRAW);

for (unsigned int i = 0; i < 4; i++) {
glEnableVertexAttribArray(1 + i);
glVertexAttribPointer(1 + i, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4),
(const GLvoid*)(sizeof(GLfloat) * i * 4));
glVertexAttribDivisor(1 + i, 1);
}

该函数的唯一变化是将实例数作为第四个参数。

对于(i=0;i<NumInstance;i++)

如果(i mod除数==0)

从带有实例数据的VBs中获取属性i/除数

对于(j=0;j<NumVertices;j++)

从带有顶点数据的VBs获取属性j

1
glDrawArraysInstanced(GL_TRIANGLES, 0, 36, 3);

顶点着色器中:

不再将WVP和世界矩阵作为统一变量,而是将它们作为顶点属性。VS不关心它们的值只会在每个实例中更新一次,而不会在每个顶点中更新。WVP矩阵占据位置1-4。

1
2
3
4
5
6
7
#version 330
layout (location = 0) in vec3 Position;
layout (location = 1) in mat4 MVP;
void main()
{
gl_Position = MVP * vec4(Position, 1.0);
};

如果使用第二种方法

顶点着色器

​ gl_InstanceID是一个内置变量,仅在VS中可用。由于计划在FS中使用它,因此必须在此处访问它,并在常规输出变量中传递它。gl_InstanceID的类型是整数,因此使用相同类型的输出变量。由于光栅化器无法对整数进行插值,必须将输出变量标记为“flat”。

1
2
3
4
5
flat out int InstanceID;
void main()
{
InstanceID = gl_InstanceID;
};

片段着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
#version 330
flat in int InstanceID;
out vec4 FragColor;
vec4 gColor[4] = {
vec4(0.5, 0.0, 0.0,0.0),
vec4(0.0, 0.5, 0.0,0.0),
vec4(0.0, 0.0, 0.5,0.0),
vec4(0.0, 0.0, 0.0,0.0)
};
void main()
{
FragColor = gColor [InstanceID % 4];
};

效果如下:

image-20230127172248210

法线贴图

表面光滑的法线:在三角形的三个顶点法向量之间进行平滑插值来得到三角形上每个点的法向量。

法线贴图:从‘法线贴图’上进行采样得到对应的法线方向,表面上的所有法向量都是可以被计算并且存储在法线贴图中。在片段着色器阶段进行光照计算的时候,每个像素的特定法线也是根据纹理坐标采样来获取使用。

切线空间:坐标系需要三个正交单位向量。物体表面确定了原法线后会有多个切线X和副切线Y,由于新法线是2D纹理的的一部分,而2D纹理有两个正交单位向量U和V,因此通常做法是将X分量对应到U轴(Tangent),而Y分量对应到V轴在(Bitangent),将切线方向与纹理空间对齐。由原法线、与纹理空间对齐的切线和副切线组成的矩阵是TBN矩阵,该矩阵向量定义的坐标系是切线空间。

法线纹理:对于新法线,即使是在非常凹凸不平的表面,仍然认为法线的方向是从纹理朝外的。在切线空间定义法向量,并存储在纹理的RGB文素中。Z分量主导的一个分量,X和Y分量只能起到让其略微倾斜的作用。将XYZ向量存储在RGB文素中会使得法线纹理偏蓝色。但是法线坐标[-1,1]需要变成rgb文素[0,1]。

切线和副切线的计算:

img

找到物体本地空间下的向量T(表示tangent)和B(表示bitangent),可以看到两个三角形边E1和E2可以写成T和B的线性组合:

img

也可以写成下面的形式:

img

现在可以很容易的转换成矩阵公式的形式:

img

现在想把矩阵转换到等式的右边,为此可以两边乘以上面标红的矩阵的逆矩阵:

img

计算如下:

img

算出逆矩阵的值得到:

img

​ 对每一个三角形执行上述过程,可以为每个三角形通过三角形的边和UV坐标计算出tangent向量和bitangent向量(对三角形的三个顶点来说这两个向量都是一样的)。通常的做法是为每一个顶点都保存一个tangent/bitangent值,每个顶点的tangent/bitangent值由共享这个顶点的所有三角面的平均tangent/bitangent值确定(这与顶点法线是一样的)。这样做的原因是使整个三角面的效果比较平滑,防止相邻三角面之间的不平滑过渡。这个坐标系空间的第三个分量——法线分量,是tangent和bitangent的叉乘积。

向着色器中将法线数组、切线数组和副切线数组作为顶点属性传入着色器,同时用GLuint类型变量加载法线纹理。

在顶点着色器中新增代码:

计算都摄像机空间中做,因为在这一空间中更容易获取片段坐标。使用3*3的模型视图矩阵乘T、B、N向量:

1
2
3
vec3 vertexTangent_cameraspace = MV3x3 * vertexTangent_modelspace;
vec3 vertexBitangent_cameraspace = MV3x3 * vertexBitangent_modelspace;
vec3 vertexNormal_cameraspace = MV3x3 * vertexNormal_modelspace;

这三个向量确定了TBN矩阵,此矩阵是从世界空间到MV下的切线空间的变换:

1
2
3
4
mat3 TBN = transpose(mat3(
vertexTangent_cameraspace,
vertexBitangent_cameraspace,
vertexNormal_cameraspace ));

利用它计算切线空间中的光线方向和视线方向:

1
2
LightDirection_tangentspace = TBN * LightDirection_cameraspace;
EyeDirection_tangentspace = TBN * EyeDirection_cameraspace;

在片段着色器中添加如下代码:

先获取法线数据,从rgb的[0,1]变换到齐次坐标的[-1,1],再进行归一化作为计算光照的n向量。由于该n向量是在切线空间计算的,因此为了保证计算都在同一个空间进行,需要在顶点着色器中相关参数TBN矩阵:

1
vec3 TextureNormal_tangentspace = normalize(texture( NormalTextureSampler, UV ).rgb*2.0 - 1.0);

阴影贴图

将深度信息渲染到纹理

image-20230106163202431

​ 首先从光源的角度来看,光源是位于左上角并且指向立方体。看图中A,B,C这3个点。当B被渲染时,它的深度值进入深度缓冲区,因为在B和光源之间没有任何东西,默认它是那条线上离光源最近的点。然而当A和C被渲染的时候,它们在深度缓冲区的同一个点上“竞争”。两个点都在同一条来自光源的直线上,所以在透视投影后,光栅器发现这两个点需要去往屏幕上的同一个像素,则C点的深度值被写入了深度缓存中。

step1:创建纹理作为阴影图

1
2
3
4
5
6
7
8
9
10
11
glGenTextures(1, &depthTexture);
glBindTexture(GL_TEXTURE_2D, depthTexture);
//GL_DEPTH_COMPONENT:每个纹素放一个单精度浮点数用于存放已经标准化后的深度值 0:暂时不提供数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, 1024, 1024, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);
//过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);

step2:创建配置帧缓冲

1
2
3
4
glGenFramebuffers(1, &FramebufferName);
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);
//GL_DEPTH_ATTACHMENT:附着在上面的纹理收到深度测试的结果,0:一个层级
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTexture, 0);

step3:相关设置

​ 禁止向颜色缓存中写入,只输出深度,默认情况下,颜色缓存会被绑定在GL_COLOR_ATTACHMENT0上,此使的帧缓冲不包含纹理缓冲区,最后确认帧缓冲的状态完整。

1
2
3
4
5
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
GLuint error = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
return false;

step4:在缓冲绘制图像

本次绘制不要片段着色器,但一定要开启深度测试(此处以点光源的mvp为例)

1
2
3
4
5
6
7
8
9
10
11
12
//将图像绘制进帧缓冲而不是显示在屏幕
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);
//从光源的角度观察,计算mvp
glm::vec3 lightInvDir = glm::vec3(0.5f, 2, -2);
glm::vec3 lightPos(5, 20, 20);
glm::mat4 depthP = glm::perspective<float>(45.0f, 1.0f, 2.0f, 50.0f);
glm::mat4 depthV = glm::lookAt(lightPos, lightPos-lightInvDir, glm::vec3(0,1,0));
glm::mat4 depthM = glm::mat4(1.0);
glm::mat4 depthMVP = depthP * depthV * depthM;
//正常绘制图像
//回到默认帧缓冲,渲染到屏幕
glBindFramebuffer(GL_FRAMEBUFFER, 0);

绘制物体

step5:在屏幕绘制图像

1
2
3
4
5
6
7
8
9
10
11
12
//将帧缓冲纹理作为正常纹理渲染
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, depthTexture);
glUniform1i(TextureID, 0);
//便于从齐次坐标[-1,1]变换到纹理坐标[0,1]
glm::mat4 biasMatrix(
0.5, 0.0, 0.0, 0.0,
0.0, 0.5, 0.0, 0.0,
0.0, 0.0, 0.5, 0.0,
0.5, 0.5, 0.5, 1.0);
//除了传入相机视角的MVP矩阵,还要传入光源视角的MVP矩阵
glm::mat4 depthBiasMVP = biasMatrix * depthMVP;

step6:着色器文件(与上一次绘制不同的着色器)

​ 其次,从摄像机的角度于每一个像素,从深度缓冲区中取出相应的深度值,同时也计算这个像素到光源的距离。如果这两个深度值不同,意味着从光源看这个像素时有其他像素遮挡了它,这种情况下在颜色计算中要增加阴影因子来模仿阴影效果。

(1)顶点着色器

1
2
ShadowCoord = DepthBiasMVP * vec4(position,1);//光源视角
gl_Position = MVP * vec4(position,1);//相机视角

​ 由于片段着色器将接收到的裁剪空间下的坐标看做一个标准的顶点属性,光栅化程序不会对其进行透视分割(只有传到gl_position变量中的顶点才会自动执行透视分割)。将这个向量除以其W分量手动透视分割;

(2)片段着色器

从阴影贴图中获取深度数据的,将上面阴影贴图的深度值和当前像素的深度值进行比较,如果阴影贴图的深度值小,也就是阴影离相机近,那么就返回0.2作为阴影参数,反之就返回1.0表示没有阴影。bias是修复参数,仅用这一个参数还是会出现粗糙的阴影,还需要更多参数调整。https://blog.csdn.net/linjf520/article/details/105380551/

float bias = 0.005;
float visibility=1.0;
if ( texture2D( sampler, (ShadowCoord.xy/ShadowCoord.w) ).x  <  (ShadowCoord.z-bias)/ShadowCoord.w ){
    visibility=0.2;
}

将阴影参数传进来并调整漫射光和镜面反射光的颜色值,环境光就不受阴影影响。

1
2
3
4
gl_FragColor.rgb = 
MaterialAmbientColor +
visibility* MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance) +
visibility*MaterialSpecularColor * LightColor * LightPower * pow(cosAlpha,5) / (distance*distance);

image-20230125133010521

天空盒

​ 天空盒子是一种让场景看上去更广阔无垠的一种视觉技术,用无缝对接的封闭纹理将摄像机的视口360度无死角的包裹起来。视角中除了真实模型的其他空余部分被封闭纹理所完全填充充当背景。

​ 天空盒子的一种实现方法是渲染一个巨大的正六面体封闭盒子纹理,并将相机置于中心,当摄像机移动的时候封闭纹理也跟着移动,所以看上去永远走不到场景中的视平线边缘。天空盒子技术除了用上面的立方体实现,还可以用球面来实现。

​ 这种纹理叫做立方体贴图(Cubemap)。为了从立方体贴图中采样,要采用3d纹理坐标而不是我们之前用的2d纹理坐标。纹理采样器将3d纹理坐标看做一个向量,找出该文素位于立方体的哪一个面上并从那个面上取出需要的文素。

创建纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static GLuint
InitTextures(void)
{
GLenum imgFormat;
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);

GLint imgWidth, imgHeight;
GLubyte* image = NULL;

image = LoadRGBImage(TexFiles[0], &imgWidth, &imgHeight, &imgFormat);

glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0,
GL_RGB, imgWidth, imgHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0,
GL_RGB, imgWidth, imgHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0,
GL_RGB, imgWidth, imgHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0,
GL_RGB, imgWidth, imgHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0,
GL_RGB, imgWidth, imgHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0,
GL_RGB, imgWidth, imgHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
free(image);

glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

return texture;
}

​ 开始先创建一个纹理对象来加载cubemap纹理,这个对象绑定到了一个特殊的GL_TEXTURE_CUBE_MAP目标对象上。设置cubemap的六个面枚举:

​ GL_TEXTURE_CUBE_MAP_NEGATIVE_X
​ GL_TEXTURE_CUBE_MAP_POSITIVE_X
​ GL_TEXTURE_CUBE_MAP_NEGATIVE_Y
​ GL_TEXTURE_CUBE_MAP_POSITIVE_Y
​ GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
​ GL_TEXTURE_CUBE_MAP_POSITIVE_Z

​ 通过glTexImage2D()函数将资源数据传给OpenGL。cubemap加载解析结束后,设置过滤。(这里简化只加载了一张图片)

着色器文件

顶点着色器

1.顶点着色器之后,光栅器将获得gl_Position向量,并进行透视分割以完成投影变换(将各分量除以W分量)。将Z分量设置成W分量的值可以保证透视分割后位置向量最终的Z分量值为1.0。Z分量为1意味着永远处于Z轴最远处,在深度测试中相对于其他物体模型天空盒子将永远处于劣势,因此天空盒子就总是作为其他物体的背景了,而其他物体会一直渲染在背景前面。

2.使用天空盒子自身坐标系中顶点的原始坐标来作为3D纹理坐标。因为对cubemap纹理采样时是从中心发射一个向量到立方体盒子或者球面上的,因此盒子表面上点的坐标恰好就是纹理坐标。

1
2
3
v_Position=vertexPosition_modelspace;
gl_Position = MVP * vec4(vertexPosition_modelspace,1);
gl_Position=gl_Position.xyww;

片段着色器

使用’samplerCube’而不是’sampler2D’以获取cubemap的纹理。

1
2
uniform samplerCube myTextureSampler;
gl_FragColor=textureCube(myTextureSampler, v_Position);

应用纹理

1.第一个要改变的是表面剔除模式。通常,会剔除掉背向相机看不到的三角形图元,而对于天空盒子来说,相机是置于盒子内部的,所以想看到盒子的内部而不是外部。用相反的OpenGL剔除模式,就要告诉OpenGL剔除去正面的三角形。

1
2
glEnable(GL_CULL_FACE);//开启表面剔除(默认背面剔除)
glCullFace(GL_FRONT);//剔除正面实现

2.第二个要改变的是深度测试函数模式。默认是告诉OpenGL,输入的片元如果比存储的片元Z值小就认为赢得深度测试而被渲染,但是对于天空盒子,Z值总是最远的边界,如果深度测试函数模式设置为‘小于’,天空盒子会被裁剪掉,为了让盒子成为场景的一部分要将深度测试函数模式改为‘小于等于’。

1
glDepthFunc(GL_LEQUAL);

3.计算WVP矩阵。对于天空盒子来说,世界坐标系的中心位于相机处,从而保证相机始终在天空盒子中心。

1
2
glm::mat4 V = glm::mat4(glm::mat3(getViewMatrix()));//天空盒v矩阵
glm::mat4 V1 = getViewMatrix();//其他物体v矩阵

cubemap的纹理贴图绑定到纹理单元0号上,设置为GL_TEXTURE_CUBE_MAP

1
2
3
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, Texture);
glUniform1i(TextureID, 0);

最后还原原本的剔除模式和深度测试函数还原

光照详解

基于平行光的环境光

​ 环境光是平行光,平行光有特定的方向但是没有特定的光源,所有的光都互相平行,计算时完全忽略光的方向,整个场景被均匀照亮。平行光的另外一个重要性质是不管它离物体多远亮度是不变的。

​ 光源的颜色定义为一个包含三个浮点数的三元组,浮点数介于[0,1]之间。光源的颜色和物体表面的颜色相乘得到反射回来的颜色。同时,加入强度,可以定义为一个[0,1]之间的一个单一的浮点数,然后和之前的相乘,从而得到最终的颜色值。

image-20230106111932045

​ 一般只是添加少量的环境光来避免出现物体一面被照亮而另一面完全是黑色的现象。例如希望物体的背后有一点亮光,因为在现实生活中灯泡会照亮它背后的墙,而墙会反过来(微弱地)照亮物体的背后,但计算这种光照的代价过大,因此通常可以简单地以假的光源代替这种计算。

基于点光源的漫反射光

​ 漫射光的特性依赖光线的方向,漫射光使物体朝向它的那一面比其他背向光的面要更亮,亮度取决于光线和物体表面的角度。光线如果要对物体表面的亮度产生影响,那么光线和法线的角度要在0-90度之间但不包含90度。这种影响通过光向量l和法线向量n的点积计算。基于点光源的漫反射,表面收到的光通量依赖于表面到光源的距离:越远光越少,与距离的平方成反比。

​ 顶点和法线都定义在本地坐标系空间,MVP变化后到裁剪空间。然而光照在世界空间中定义,所以在计算之前首先要将法线向量变换到世界坐标系空间。

​ 一个多边形面上分布的任意法向量都是一样的,足以用其中一个代表来计算顶点着色器中的漫射光。但有时一个顶点的多个面法线不同。需要使用到一个概念叫做‘顶点法线’,顶点法线是共用一个顶点的所有三角形法线的平均值。将顶点法线作为一个成员属性传给片段着色器。光栅器会得到三个不同的法向量并对其之间进行插值运算。片段着色器将会对每个像素计算其特定的插值法向量对应的颜色值,对漫射光的计算可以达到像素级别。效果是光照效果在每个相邻三角形面之间会平滑的变化。

基于点光源的镜面反射光

​ 基于点光源的镜面反射,比起漫反射还包含了观察者的位置。镜面反射时光以一定角度照射到物体表面,同时会在法线的另一侧对称的角度上反射出去,如果观察者刚好在反射光线的路径上那么就会看到格外强烈的光线。镜面反射最终的结果是物体在从某个角度看上去会十分明亮,而移动开后这个光亮又会消失。镜面反射光的存在更取决于反射物体的材料性质而不是光源本身。

image-20230106144515706

​ 亮度取决于观察者和反射光的夹角,随着角度增大反射光衰弱。这种影响通过R和V的点积计算。随着夹角’α’增大余弦值慢慢减小,直到夹角达到90°时无镜面反射的效果,夹角大于90°时余弦值为负,也没有任何反射效果,也就是观察者不在反射光的路径范围内。

反射光线’R’使用’I’向量来计算,如下图:

image-20230106144537465

​ 向量没有起点的概念,所有方向相同且长度相同的向量都是同一个向量。因此,图中将入射光向量’I’复制到表面下面位置向量本身是不变的。根据向量的加法,’R’等于’I’+’V’,’I’已知求’V’。法线’N’的反向向量为’-N’,计算’I’和’-N’的点积可以得到’I’在’-N’上的投影,这是’V’的模长度的一半。另外’V’和’N’的方向是相同的,所以只要用计算的那个投影长度乘以单位向量’N’再乘以2就是向量’V’了。用公式简单表示如下,通过’reflect’内部函数计算:

image-20230106144604599

​ 计算镜面反射的最终公式:

image-20230106144620480

​ 开始先是将光的颜色和物体表面的颜色相乘,这个和在计算环境光以及漫反射光时一样。得到的结果再和材料的镜面反射强度参数(’M’)相乘。如果材料没有反射性能,比如木头,那么镜面反射参数就为0,而像金属这种发光材料镜面反射能力就会很强。之后再乘以光线和观察者视线夹角的余弦值,即镜面参数’或者叫做‘发光参数’,用来增强加剧反射光区域边缘的强度。

聚光灯光源

​ 聚光灯光源也会随着距离衰减,相当于取点光源的一个锥形的一小部分,聚光灯光源呈锥形,离光源越远,照亮的圆形区域会越大(光源位于锥形体的尖端)。如下图:

image-20230106144647823

​ 图中L是光源方向,实现让光源只照亮两条红线夹角之间的区域。可以定义光锥为光线方向L和红线之间的夹角(两条红线之间夹角的一半)。点积计算夹角的余弦值‘C’以及L和V夹角的余弦,其中V指的是光源到某个像素的向量,如果后者的值大于余弦值‘C’,说明L和V之间的夹角偏小,该像素就位于被照亮的区域内。反之,像素位于区域外就不会被该光源照亮。

​ 如果仅按照上面说的在照亮区域内就点亮像素,否则就不点亮,照亮区域和未照亮区域之间的边界边缘会非常明显。一个真实的聚光灯光源会从照亮区域的中心向圆形边缘慢慢衰减。可以利用上面计算得到的那些点积作为一个衰减的参数。但是用余弦来做衰减参数会有问题,因为聚光灯光源的夹角不能太大,否则范围太广就失去了聚光灯的效果,但是在夹角从0到一个比较小的角度范围内,cos值得变化是很缓慢的,导致衰减不明显。要想衰减效果明显这个参数范围应该是[0,1]。解决方法是将这个参数的小范围映射到[0,1]的范围。

​ 聚光灯光源需要光源的方向向量和截断光源照亮范围的一个阈值。阈值代表的是光源方向向量和光源到可照亮像素之间的最大夹角。比这个阈值夹角大的像素是不会被该光源照亮的。

​ 首先得到光源到某个像素的向量,将向量单位化方便点积运算,然后和单位化了的光源方向向量进行点积运算得到他们之间夹角的余弦值。将得到的余弦值和光源的阈值(定义光源范围的最大夹角的余弦值)进行比较,如果余弦值比阈值小,说明夹角太大像素在照亮圆区域的外面,这样像素就不会被该光源点亮。反之如果像素在照亮区域内,我们就先像点光源那样计算光源的基础颜色。然后将计算的余弦值插值到0到1的范围,最后和点光源颜色相乘计算得到最终的聚光灯颜色值。

基于点光源的着色器文件编写

顶点着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
attribute vec3 vertexPosition_modelspace;
attribute vec2 vertexUV;
attribute vec3 vertexNormal_modelspace;//法线数组

varying vec2 UV;
varying vec3 Position_worldspace;
varying vec3 Normal_cameraspace;
varying vec3 EyeDirection_cameraspace;
varying vec3 LightDirection_cameraspace;

uniform mat4 MVP;
uniform mat4 V;
uniform mat4 M;
uniform vec3 LightPosition_worldspace;

void main(){

gl_Position = MVP * vec4(vertexPosition_modelspace,1);

//计算世界空间下的物体矩阵(便于等下计算世界空间下光与表面的距离)
Position_worldspace = (M * vec4(vertexPosition_modelspace,1)).xyz;

//计算观察空间下的物体矩阵
vec3 vertexPosition_cameraspace = ( V * M * vec4(vertexPosition_modelspace,1)).xyz;

EyeDirection_cameraspace = vec3(0,0,0) - vertexPosition_cameraspace;

//观察空间下光的位置
vec3 LightPosition_cameraspace = ( V * vec4(LightPosition_worldspace,1)).xyz;

//观察空间下表面到点光源的向量
//如果是基于平行光的漫反射此处直接归一化光的方向
LightDirection_cameraspace = LightPosition_cameraspace + EyeDirection_cameraspace;

//计算观察空间下的法线数组
Normal_cameraspace = ( V * M * vec4(vertexNormal_modelspace,0)).xyz;

UV = vertexUV;
}

片段着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#version 120

varying vec2 UV;
varying vec3 Position_worldspace;
varying vec3 Normal_cameraspace;
varying vec3 EyeDirection_cameraspace;
varying vec3 LightDirection_cameraspace;

uniform sampler2D myTextureSampler;
uniform mat4 MV;
uniform vec3 LightPosition_worldspace;

void main(){

//光照颜色和强度
vec3 LightColor = vec3(1,1,1);
float LightPower = 50.0f;

//材质本身的颜色也影响最终颜色
//漫反射分量
vec3 MaterialDiffuseColor = texture2D( myTextureSampler, UV ).rgb;
//环境光分量
vec3 MaterialAmbientColor = vec3(0.1,0.1,0.1) * MaterialDiffuseColor;
//镜面光分量
vec3 MaterialSpecularColor = vec3(0.3,0.3,0.3);

//计算光与表面的距离:光通量与距离的平方成反比(世界空间下)
float distance = length( LightPosition_worldspace - Position_worldspace );

//归一化n和l(观察空间下计算(可以是任意空间))
vec3 n = normalize( Normal_cameraspace );
vec3 l = normalize( LightDirection_cameraspace );

//n:表面法线,l表面到光源的单位向量(与光相反,简化计算),二者计算点积
//如果光源在三角形后面,n和l方向相反,那么n.l是负值。这意味着colour将是一个负值,没有意义。因此这种情况下必须用clamp()将cosTheta截取为0:
float cosTheta = clamp( dot( n,l ), 0,1 );

//E:观察者,R反射光,二者计算点积
vec3 E = normalize(EyeDirection_cameraspace);
vec3 R = reflect(-l,n);//内置函数计算反射光
float cosAlpha = clamp( dot( E,R ), 0,1 );

//最终颜色计算
//聚光灯需要添加余弦参数
gl_FragColor.rgb =
MaterialAmbientColor +
MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance) +
//pow(cosAlpha,5)用来控制镜面反射的波瓣,镜面反射与材料本身有关
MaterialSpecularColor * LightColor * LightPower * pow(cosAlpha,5) / (distance*distance);
}

step_by_step实例学习

矩阵变换

平移

image-20230106090323761

​ 因为所有的位移值都要乘以向量的w行,所以位移值会加到向量的原始值上。这样使用一个4维向量表示一个3维向量叫做齐次坐标。通常对于表示点的矩阵会让w=1,而对于表示向量的矩阵会让w=0,因为点可以被做变换而向量不可以,可以改变一个向量的长度和方向,但是长度和方向一样的所有向量都是相等的,不管他们的起点在哪里,所以可以把所有的向量起点放到原点来看。对于向量设置w=0然后乘以变换矩阵会得到和自身一样的向量。

旋转

image-20230106093340662

旋转后的坐标变换

image-20230106093352086 image-20230106093404574

旋转矩阵

绕z轴

image-20230106093419847

绕y轴

image-20230106093429878

绕z轴

image-20230106093440038

更好的方法是四元组

缩放

进行缩放变换其实很简单。我们从最开始的原变换矩阵来看,回忆平移变换矩阵的样子,我们保持结果矩阵中V1,V2和V3保持原样的办法是让变换矩阵主对角线上的值都为’1’,这样原向量一次都和1相乘之后依然保持不变,各分量之间互不影响。所以,这里的缩放变换,只要把那些‘1’换成我们想缩放的值,原向量各分量分别乘以这些值之后就会在相应坐标轴上进行相应的缩放了,值大于1则放大,值小于1则缩小。

如果片断着色器没有显式地请求那个变量(你可以使用同一个顶点着色器混合并匹配多个片断着色器)那么一般的驱动优化会丢弃顶点着色器vs中只是影响该变量的操作(特定的shader程序是针对vs和fs的配对组合)。但如果片断着色器fs确实使用到了那个变量,光栅器会在光栅化阶段对其进行插值,并且每一次片断着色器fs的调用都会提供一个匹配特定位置的插值后的值,这意味着相邻的两个像素的值都略有不同(虽然随着三角形离摄像头越来越远那样会越来越不合适)。

***经常情况下依赖插值的两个变量是三角形的法向量和纹理坐标***。顶点的法向量通常是计算包含这个顶点的所有三角形法向量的平均值而得。如果物体不是平坦的话那么每个三角形的三个顶点的法向量会各不相同,那样我们可以通过插值来计算每个像素的法向量,那些向量会用于光线的计算,从而产生更逼真可信的光照效果。插值对于纹理坐标的应用也类似,这些坐标作为模型的一部分定义在每个顶点上。为了用贴图覆盖三角形你必须对每个像素进行一样的插值操作并给每个像素定义正确的纹理坐标,这些坐标都是插值的结果

复合变换

1
2
3
4
glm::mat4 Rotation_tree2 = eulerAngleYXZ(gOrientation1.y, gOrientation1.x, gOrientation2.z);
glm::mat4 Transla_tree2 = translate(mat4(), gPosition2);
glm::mat4 Scaling_tree2 = scale(mat4(), vec3(0.3f, 0.3f, 0.3f));
glm::mat4 Model_tree2 = Transla_tree2 * Rotation_tree2 * Scaling_tree2;

采样器采样数据

纹理

​ 纹理是一个2D图片(甚至也有1D和3D的纹理),可以用来添加物体的细节。因此可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。除了图像以外,纹理也可以被用来储存大量的数据,这些数据可以发送到着色器上。纹理采样的本质是利用采样器读取数据,所读取的数据不一定是作为纹理渲染。

采样

​ 为了能够把纹理映射到三角形上,需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(UV坐标),用来标明该从纹理图像的哪个部分采样。之后在图形的其它片段上进行片段插值。纹理坐标在x和y轴上,范围为0到1之间(2D)。使用纹理坐标获取纹理颜色叫做采样。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了如何把纹理坐标映射到三角形上的。

image-20230105085456442

纹理单元

一个纹理的位置值通常称为一个纹理单元。一个纹理的默认纹理单元是0,它是默认的激活纹理单元。纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。激活纹理单元之后,接下来绑定这个纹理到当前激活的纹理单元,纹理单元GL_TEXTURE0默认总是被激活。

1
2
3
4
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);

定义哪个采样器对应哪个纹理单元,只需要设置一次即可

1
2
glUniform1i(Texture1ID, 0);
glUniform1i(Texture2ID, 1);

作为其他数据采样

先利用phrtest03的函数自定义一组纹理数据:

1
2
3
4
float cube1[4] = { 1.0, -1.0,1.0,0.8};
float cube2[4] = { 0.7, 0.8,1.0,0.1};
float cube3[4] = { -1.0, 0.5, 1.0,0.3};
float cube4[4] = { 0.4, 0.8,1.0,0.6};

该纹理在1:1的四边形上分成了四块

纹理坐标:

1
2
3
4
5
6
static const GLfloat tex_data[] = {
0.0f,1.0f,
1.0f,1.0f,
1.0f,0.0f,
0.0f,0.0f
};

画四个柱形,分别在四块进行采样,因此现在顶点着色器中对UV坐标进行变换(变换方式详见之前的分屏变换)

1
2
3
4
5
6
7
8
9
10
11
  UV = vertexUV;
if (position.x<=-0.2) {
UV.x = UV.x /2.0;UV.y = UV.y /2.0-0.001;
}else{
if (position.x<0.1){
UV.x = UV.x /2.0+0.5; UV.y = UV.y /2.0;
}else{
if (position.x<0.4){
UV.x = UV.x /2.0;UV.y = UV.y /2.0+0.5;
}else{
UV.x = UV.x /2.0+0.5;UV.y = UV.y /2.0+0.5; }}}

在顶点着色器中设置采样器(如果只有一个纹理可以和片段着色器中的采样器同名),将纹理数据的第四个值作为y轴坐标,注意先声明uniform sampler2D sampler;

1
2
3
4
5
float y=texture2D(sampler, UV).a;
if(position.y>0.0){
gl_Position = vec4(position.x,y,position.z,1);
}else{
gl_Position = vec4(position,1);}

因为要设置光照,在片段着色器中将纹理数据的第四个值作为颜色,将前三个值作为法线数据

1
2
3
4
float color=texture2D( sampler, UV ).a;
vec3 MaterialDiffuseColor=vec3(color,0.0,0.0);
vec3 normal = texture2D( sampler, UV ).rgb;
vec3 Normal_cameraspace = ( V * M * vec4(normal,0)).xyz;

运行结果:

image-20230105094422261

因此纹理数据可以作为任意顶点数据使用,采样器在顶点着色器和片段着色器都可以使用。

多级纹理

纹理坐标

​ 如果为顶点指定的纹理坐标不在(0,1)的范围内,需要设置纹理环绕方式

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

​ 环绕方式使用glTexParameter*函数对每个坐标轴进行设置(s,t(如果是3d纹理还有一个r,他们和xyz等价))

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

纹理过滤

纹理坐标不是离散的,可以是任意浮点值,而纹理就是一个个像素点组成的图片,纹理像素是离散的。纹理过滤是纹理采样时纹理坐标和纹理像素之间的一种映射方式(它使得同一个纹理可以被用于不同的形状,尺寸和角度,同时尽可能减少显示时的模糊和闪烁)。 最常用的两种方式:

1.GL_NEAREST,邻近过滤,这是OpenGL默认的过滤方式,它是选择中心点即小正方形中心点距离纹理坐标最近的那个纹理像素作为采样结果。

2.GL_LINEAR,线性过滤,它会基于纹理坐标附近的纹理像素计算一个插值,而纹理像素中心距离纹理坐标越近,那么该纹理像素在采样结果中占有的权重就越大。

当进行放大缩小操作的时候,可以设置纹理过滤的选项。

1
2
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

多级渐远纹理

当要表示的物体很远的时候,如果我们用贴图的话就会造成纹理像素远远高于屏幕像素的情况,这个时候要从高分辨率的贴图中获取正确的颜色值就很困难。OpenGL使用多级渐远纹理(Mipmap)来解决这个问题,它就是一系列的纹理图像,后一个纹理是前一个纹理的1/2,依次类推。它的原理就是把摄像机到物体的距离与阙值作比较,在不同的距离空间内选用不同的纹理图像。(近距离的数据多更加细致)

image-20230103162452990

创建多级纹理:

1
2
3
4
5
6
7
//gluBuild2DMipmaps(GL_TEXTURE_2D, 4, imgWidth, imgHeight,imgFormat, GL_UNSIGNED_BYTE, image);
//用载入的图片生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, imgWidth, imgHeight, 0, imgFormat, GL_UNSIGNED_BYTE, image);
//创建多级纹理
glGenerateMipmap(GL_TEXTURE_2D);
//释放图像内存
free(image);

多级渐远过滤方式

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

例子:

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

对比:

使用多级纹理

image-20230103165513744

不使用多级纹理

QQ图片20230104113400

TransformFeedback

变换反馈

将顶点数据发给着色器,变换后从着色器获取数据作为新的顶点数据进行绘制,可以用于绘制粒子及动画效果

一、shader.cpp文件

1
2
const GLchar* feedbackVaryings[] = { "outposition","fragcolor"};
glTransformFeedbackVaryings(ProgramID, 2, feedbackVaryings, GL_SEPARATE_ATTRIBS);

这两行代码需要放在glLinkProgram(ProgramID);之前。第一个数组说明了捕获到一个缓冲区中的输出属性(与顶点着色器中需要捕获的out的名字相同)。第二个函数的第一个参数是着色器程序;第二个参数和第三个参数指定了输出名称数组和数组本身的长度,最后一个参数指定了应该如何写入数据。以下两种格式可供选择:

1.–GL_INTERLEAVED_ATTRIBS:将所有属性写入一个缓冲区对象。

2.–GL_SEPARATE_ATTRIBS: 将属性写入多个缓冲区对象,或将不同的偏移量写入缓冲区。

比如一个程序有颜色和顶点两个缓冲区,使用第一个参数只能绑定一个缓冲区,此时想变换颜色和顶点需要把这两组数据写在同一个数组并设置读取的偏移量:

1
2
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, databufferb);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, databuffera);

使用第二个参数就可以将颜色数据和顶点数据分开,将两个缓冲区绑定到trasnformfeedback上:

1
2
3
4
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, databufferb);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, databuffera);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, colorbufferb);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, colorbuffera);

其中第二个参数就是缓冲区索引,绑定两个的话,第一个缓冲区索引是0,第二个是1,可以区分缓冲区。

二、顶点着色器文件

1
2
3
4
5
6
in vec4 inposition;
in vec4 color;
out vec4 outposition;
out vec4 fragcolor;//与feedbackVaryings[]同名
/*在main函数中对inposition和color进行变换*/
gl_Position=MVP * inposition;

输入的顶点属性数据inposition经过MVP变换后作为最终的gl_Position数据用来绘制图形;将inposition数据变换后存入outposition写入tf缓冲。(color同理)

三、应用程序文件

以GL_INTERLEAVED_ATTRIBS参数为例:

1
2
3
4
5
6
7
//tf缓冲区只用创建一个
GLuint tfbuffera;
glGenTransformFeedbacks(1, &tfbuffera);
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, tfbuffera);
//给tf缓冲绑定相应的缓冲对象(databuffer用GL_ARRAY_BUFFER类型创建)
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, databufferb);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, databuffera);

相同的缓存不能同时作为输入和输出,所以如果想产生连续变换,需要两个缓冲区交换数据。所以需要两个databuffer。第一个buffer绑定data数据用于绘制第一个图形,第二个传递了一个nullptr,以创建一个足够大的缓冲区。(比如粒子系统生成的定点数会越来越多,此处不需要考虑该问题)

1
2
glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_DYNAMIC_DRAW);
glBufferData(GL_ARRAY_BUFFER, sizeof(data), nullptr, GL_DYNAMIC_DRAW);

主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//绑定
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, tfbuffera);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, databuffer[flag]);//将变换后数据写入databufferb
glEnable(GL_RASTERIZER_DISCARD);//此次绘制写入tf缓冲因此不进行绘制,禁用光栅化
glBeginTransformFeedback(GL_TRIANGLES);//开启tf缓冲,设置绘制模式与draw相同
//用databuffera也就是最初的数据传入顶点着色器,数据进行第一次变换,但此时不显示,而是写入tf缓冲的databufferb
glEnableVertexAttribArray(positionID);
glBindBuffer(GL_ARRAY_BUFFER, databuffer[!flag]);
glVertexAttribPointer(positionID,4,GL_FLOAT,GL_FALSE,32,(void*)0);
glVertexAttribPointer(colorID,4,GL_FLOAT,GL_FALSE,32,(void*)16);
glDrawArrays(GL_TRIANGLES, 0, 36);
//结束转换反馈模式
glEndTransformFeedback();
//使用光栅化,绘制图形
glDisable(GL_RASTERIZER_DISCARD);
//使用databufferb第一次变换的数据绘制图形,因为此使没开启tf缓冲,因此顶点顶点着色器的数据变换不写入缓冲
glBindBuffer(GL_ARRAY_BUFFER, databuffer[flag]);
glVertexAttribPointer(positionID,4,GL_FLOAT,GL_FALSE,32,(void*)0);
glVertexAttribPointer(colorID,4,GL_FLOAT,GL_FALSE,32,(void*)16);
glDrawArrays(GL_TRIANGLES, 0, 36);
flag = !flag;//进行第二次循环

第二次循环将变换后的数据写入databuffera,将databufferb第一次变换的数据传入顶点着色器,数据进行第二次变换,但此时不显示,而是写入tf缓冲的databuffera。使用databuffera第二次变换的数据绘制图形,因为此使没开启tf缓冲,因此顶点顶点着色器的数据变换不写入缓冲。

原始数据:-0.1f,0.1f, 0.1f,1.0f,0.1f,0.1f, 0.1f,1.0f

如下是经过两次循环后tf缓冲中的数据:

image-20230102175536791

补充

如下数据每行前四个是顶点坐标数据,后四个是顶点颜色数据:

image-20230102180412855

第五个参数:步长(Stride),在连续的顶点属性组之间的间隔。上图一行8个数是一个顶点属性,所以步长为8*4=32

第六个参数:使用VBO时是缓冲对象数据存储区的字节偏移量(不使用VBO时是顶点数组指针)

颜色数据从第五个数据读起,前面有四个顶点坐标数据,因此偏移量为4*4=16

1
2
glVertexAttribPointer(positionID,4,GL_FLOAT,GL_FALSE,32,(void*)0);
glVertexAttribPointer(colorID,4,GL_FLOAT,GL_FALSE,32,(void*)16);

参考链接:https://blog.csdn.net/niu2212035673/article/details/79041437

请我喝杯咖啡吧~

支付宝
微信