数据 - 纹理

在这一篇文章中,我们开始介绍纹理。在此之前,我们也有一个小铺垫:在 《KTX 格式和 SBM 格式》 中,我们可以大致了解书中使用的一种纹理数据封装格式。

书中关于纹理的示例非常多,这非常好,相关的知识点会更加直观。之后的内容也把示例代码作为组织顺序,记录各个知识点。

1. simpletexture

如代码片段 1.1 所示,设置纹理相关的内容,和设置缓冲相关内容很相似。glCreateTextures 创建一个纹理名称。glBindTexture 将纹理和绑定点绑定,以便在着色器中使用。glTexStorage2D 分配存储纹理的空间。glTexSubImage2D 传递纹理数据。

代码片段 1.1 设置纹理
  • void startup()
  • {
  •     // Create a name for the texture
  •     glCreateTextures(GL_TEXTURE_2D, 1, &texture);
  •  
  •     // Now bind it to the context using the GL_TEXTURE_2D binding point
  •     glBindTexture(GL_TEXTURE_2D, texture);
  •  
  •     // Specify the amount of storage we want to use for the texture
  •     glTexStorage2D(GL_TEXTURE_2D// 2D texture
  •                     8,              // 8 mipmap levels
  •                     GL_RGBA32F,     // 32-bit floating-point RGBA data
  •                     256, 256);      // 256 x 256 texels
  •  
  •     // Define some data to upload into the texture
  •     float* data = new float[256 * 256 * 4];
  •  
  •     // generate_texture() is a function that fills memory with image data
  •     generate_texture(data, 256, 256);
  •  
  •     glTexSubImage2D(GL_TEXTURE_2D// 2D texture
  •                      0,              // Level 0
  •                      0, 0,           // Offset 0, 0
  •                      256, 256,       // 256 x 256 texels, replace entire image
  •                      GL_RGBA,        // Four channel data
  •                      GL_FLOAT,       // Floating point data
  •                      data);          // Pointer to data
  •  
  •     // Free the memory we allocated before - GL now has our data
  •     delete[] data;
  •  
  •     TUtils::TShaderFileInfo shaderInfos[] =
  •     {
  •          {GL_VERTEX_SHADER,   "vs.vert"},
  •          {GL_FRAGMENT_SHADER, "fs.frag"}
  •     };
  •     program = TUtils::CompileShaders(shaderInfos,
  •         sizeof(shaderInfos) / sizeof(shaderInfos[0]));
  •  
  •     glCreateVertexArrays(1, &vao);
  •     glBindVertexArray(vao);
  • }

所使用的纹理是使用代码自动生成的,可见代码片段 1.2 中的 generate_texture 函数。图 1 是生成的 256x256 纹理的样子。

代码片段 1.2 代码生成纹理
  • void generate_texture(float* data, int width, int height)
  • {
  • #if 0
  •     char out_data[4];
  •     std::ofstream out("texture.rgb", std::ios::binary);
  •     assert(out.is_open());
  • #endif
  •     int x, y;
  •     for (y = 0; y < height; y++)
  •     {
  •          for (x = 0; x < width; x++)
  •          {
  •              data[(y * width + x) * 4 + 0] = (float)((x & y) & 0xFF) / 255.0f;
  •              data[(y * width + x) * 4 + 1] = (float)((x | y) & 0xFF) / 255.0f;
  •              data[(y * width + x) * 4 + 2] = (float)((x ^ y) & 0xFF) / 255.0f;
  •              data[(y * width + x) * 4 + 3] = 1.0f;
  • #if 0
  •              out_data[0] = (x & y) & 0xFF;
  •              out_data[1] = (x | y) & 0xFF;
  •              out_data[2] = (x ^ y) & 0xFF;
  •              out_data[3] = 0xFF;
  •              out.write(out_data, sizeof(out_data));
  • #endif
  •          }
  •     }
  • #if 0
  •     out.close();
  • #endif
  • }
图1 代码生成的纹理

接下来看着色器部分的代码。此代码示例使用了顶点着色器和片段着色器,顶点着色器是硬编码的三角形的 3 个顶点,这没有什么“新意”,代码就不贴出来了。我们看有所不同的片段着色器,如代码片段 1.3 所示。

代码片段 1.3 片段着色器
  • #version 430 core
  •  
  • uniform sampler2D s;
  •  
  • out vec4 color;
  •  
  • void main(void)
  • {
  •     //color = texelFetch(s, ivec2(gl_FragCoord.xy), 0);
  •     color = texture(s, gl_FragCoord.xy / textureSize(s, 0));
  • }

如代码片段 1.3 的片段着色器所示,纹理数据定义在类型为 sampler2D 的统一变量中。此时输出的颜色从纹理中获取,代码中展示了可以通过 texelFetchtexture 函数获取。

texelFetch 的第一个参数指定纹理统一变量,第二参数指定纹理坐标,第三个参数指定 mip 图层。需要注意的是 texelFetch 对应的纹理坐标是整型,和图片的长宽尺寸一一对应。此处如果使用 texelFetch 函数,则会出现三角形只有一小部分有贴图的现象。因为纹理图片像素很小,只有 256x256,而窗体的像素很大。

texture 的第一个参数指定纹理统一变量,第二参数指定纹理坐标。需要注意的是 texture 对应的纹理坐标是浮点型,范围为 0 到 1,所以这边除以纹理图片的大小进行简单的归一化。而纹理的大小通过 textureSize 函数获取,第一个参数指定纹理变量,第二个参数指定 mip 层。

图 2 是此示例运行的结果,可以在图中看到图 1 贴图的“影子”。

图2 运行结果

纹理坐标实验

以上纹理是如何映射的细节没有琢磨,同时书中此章的示例中都没有介绍纹理坐标的概念。在此稍微补充一下。

看到代码片段 1.4,其中定义了两个三角形:右下角一个、左上角一个。前面已经讲过 texture 指定的纹理坐标是 0 到 1,即将图片的宽和高按比例放在 (0,1) 范围内。我们将每个顶点映射到图片上的坐标点,OpenGL 就会“自动绘制”这个区域的图片颜色。

代码片段 1.4 硬编码纹理坐标
  • #version 420 core
  •  
  • out vec2 ts;
  • void main(void)
  • {
  •     const vec4 vertices[] = vec4[](vec4( 0.75, -0.75, 0.5, 1.0),
  •                                    vec4(-0.75, -0.75, 0.5, 1.0),
  •                                       vec4( 0.75,  0.75, 0.5, 1.0),
  •                                       vec4(-0.75, -0.75, 0.5, 1.0),
  •                                       vec4(-0.75,  0.75, 0.5, 1.0),
  •                                       vec4( 0.75,  0.75, 0.5, 1.0));
  •  
  •     const vec2 texes[] = vec2[](vec2(1, 1),
  •                                 vec2(0, 1),
  •                                    vec2(1, 0),
  •                                    vec2(0, 1),
  •                                    vec2(0, 0),
  •                                    vec2(1, 0));
  •  
  •     gl_Position = vertices[gl_VertexID];
  •     ts = texes[gl_VertexID];
  • }

图片的坐标系目前也没太弄明白,网上说 OpenGL 纹理图片的坐标系在左下角。但是按照这个方式对顶点进行映射的话,如图 3 所示,图片会反着,需要将 y 轴上的坐标“镜像”一下。

图3 注意纹理坐标

我想书中没讲解纹理坐标映射应该是有琐碎之处(因为我目前也不太明白图片为什么会反过来😂)。

这些暂不影响 OpenGL 的原理学习,把纹理坐标先交给 Maya 这些现成的软件吧!

我发现类 generate_texture 的函数有妙用:可以检查是读取的纹理有问题,还是 glTexStorage2D 等函数的参数设置有问题。

2. simpletexcoords

这个例子是我们首次接触到书籍作者的 sb7::ktxsb7::object 库。sb7::ktx 用于加载纹理数据,此例中加载逗号符号状的纹理;sb7::object 用于加载顶点数据,此例中加载甜甜圈状的模型。

正如在 《KTX 格式和 SBM 格式》 中所说的,调用封装的库就会有一种“自己什么都没做”、“不知道怎么就可以了”的感觉。我们借此例来了解这两个库,了解的方式是采用之前学习的内容自己实现一遍。因此原书的代码在这边就不贴出来了。

首先我们先看自己写的纹理加载函数。如代码片段 2.1 所示,虽然参数穿的有点多,但是流程非常简单,我们在第一个 simpletexture 例子中已经学习过了:glCreateTextures 创建纹理;glBindTexture 绑定纹理用于后续操作;glTexStorage2D 分配纹理空间;glTexSubImage2D 传递纹理数据。

代码片段 2.1 自定义加载纹理
  • GLuint TUtils::LoadTexture(
  •     std::string&& texPath,
  •     uint32_t width,
  •     uint32_t height,
  •     GLenum internalfmt,
  •     GLenum fmt,
  •     GLenum type)
  • {
  •     GLuint tex;
  •     glCreateTextures(GL_TEXTURE_2D, 1, &tex);
  •     glBindTexture(GL_TEXTURE_2D, tex);
  •     glTexStorage2D(GL_TEXTURE_2D, 1, internalfmt, width, height);
  •  
  •     std::ifstream in(texPath, std::ios::binary);
  •     assert(in.is_open());
  •     in.seekg(0, std::ios::end);
  •     uint32_t size = in.tellg();
  •     in.seekg(0, std::ios::beg);
  •     char* texData = new char[size];
  •     std::unique_ptr<char[]> pAuto(texData);
  •     in.read(texData, size);
  •     in.close();
  •     glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
  •          width, height, fmt, type, texData);
  •  
  •     return tex;
  • }

接着我们看自己写的顶点加载函数,这些内容我们在 《数据 - 缓冲》 中也已经学习过了。如代码片段 2.2 所示,流程为:glCreateBuffers 创建缓冲;glNamedBufferStorage 使用名称分配缓冲空间大小;写内容通过 glMapNamedBuffer 函数,方便后续文件读直接操作,不要忘记 glUnmapNamedBuffer;glVertexArrayAttribBinding 建立着色器顶点属性和绑定点之间的联系;glVertexArrayVertexBuffer 建立缓冲和绑定点之间的联系,同时指明缓冲属性;glVertexArrayAttribFormat 进一步指定着色器中的顶点属性的属性。

代码片段 2.2 自定义加载顶点数据
  • GLuint TUtils::LoadVAOAttrib(
  •     GLuint vao,
  •     std::string&& attribBinPath,
  •     uint32_t size,
  •     GLenum type,
  •     GLuint attribIndex,
  •     GLuint bindingindex,
  •     OUT uint32_t* verticesCnt)
  • {
  •     std::ifstream in(attribBinPath, std::ios::binary);
  •     assert(in.is_open());
  •     in.seekg(0, std::ios::end);
  •     uint32_t binSize = in.tellg();
  •     in.seekg(0, std::ios::beg);
  •  
  •     GLuint buffer;
  •     glCreateBuffers(1, &buffer);
  •     glNamedBufferStorage(buffer, binSize, NULL, GL_MAP_WRITE_BIT);
  •     void* ptr = glMapNamedBuffer(buffer, GL_WRITE_ONLY);
  •     in.read((char*)ptr, binSize);
  •     glUnmapNamedBuffer(buffer);
  •  
  •     int typeSize = 0;
  •     if (type == GL_FLOAT)
  •          typeSize = sizeof(float);
  •     assert(typeSize != 0);
  •  
  •     glVertexArrayAttribBinding(vao, attribIndex, bindingindex);
  •     glVertexArrayVertexBuffer(vao, bindingindex, buffer, 0, typeSize * size);
  •     glVertexArrayAttribFormat(vao, attribIndex, size, type, GL_FALSE, 0);
  •     glEnableVertexArrayAttrib(vao, attribIndex);
  •  
  •     if (verticesCnt)
  •     {
  •          if (*verticesCnt != 0)
  •              assert(*verticesCnt == binSize / (size * typeSize));
  •          *verticesCnt = binSize / (size * typeSize);
  •     }
  •     return buffer;
  • }

不要忘了 glUnmapNamedBuffer,着色器会运行不了,排查也困难。

如代码片段 2.3 所示,我们使用自己写的函数替换书中的库函数。如图 4 所示,最终的运行结果和替换前是一样的。同时自定义的函数顶点属性索引可以自行更改,书中的索引需要遵循 SBM 文件中的布局情况。

代码片段 2.3 使用自定义函数
  1. //tex_object[1] = sb7::ktx::file::load("D:\\OpenGL\\superbible7-media\\textures\\pattern1.ktx");
  2. tex_object[1] = TUtils::LoadTexture("pattern1_1024x1024_RGBA8888.rgb", 1024, 1024, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE);
  3. //object.load("D:\\OpenGL\\superbible7-media\\objects\\torus_nrms_tc.sbm");
  4. glCreateVertexArrays(1, &vao);
  5. glBindVertexArray(vao);
  6. buffer[0] = TUtils::LoadVAOAttrib(vao, "torus_nrms_tc_Position(12288).bin", 4, GL_FLOAT, 0, 0, &vexCnt);
  7. buffer[1] = TUtils::LoadVAOAttrib(vao, "torus_nrms_tc_Map(12288).bin", 2, GL_FLOAT, 4, 1, &vexCnt);
图4 运行结果

3. tunnel

这个例子展示一个隧道,介绍 mip 贴图。mip 贴图用于解决闪烁(混叠伪影)的效果。我们看到视频 1,当我们没有使用 mip 贴图时(远近都使用同一张贴图时),尤其当物体移动时,会产生波光粼粼的闪烁现象。

视频 1 闪烁现象

mip 映射纹理在贴图层面上的概念非常简单,就是提供不同大小尺寸的贴图:每个图像在上一张图片的宽高上缩小 1/2。我们简单的将各种分辨率的图片传递给 OpenGL,就能享受到这种强大的纹理技术。

传递的方法我们之前也学习过了,之前和纹理相关的函数中会有跟 mip 相关的参数。当时不了解,现在就可以了解了。我们借自己改进的纹理加载函数,再次学习原先 OpenGL 函数中关于 mip 贴图的地方。

代码片段 3.1 加载 mip 纹理
  • GLuint TUtils::LoadTexture(
  •     std::string&& texPath,
  •     uint32_t width,
  •     uint32_t height,
  •     GLenum internalfmt,
  •     GLenum fmt,
  •     GLenum type,
  •     uint32_t mip)
  • {
  •     GLuint tex;
  •     glCreateTextures(GL_TEXTURE_2D, 1, &tex);
  •     glBindTexture(GL_TEXTURE_2D, tex);
  •     glTexStorage2D(GL_TEXTURE_2D, mip, internalfmt, width, height);
  •  
  •     std::ifstream in(texPath, std::ios::binary);
  •     assert(in.is_open());
  •     in.seekg(0, std::ios::end);
  •     uint32_t size = in.tellg();
  •     in.seekg(0, std::ios::beg);
  •     char* texData = new char[size];
  •     std::unique_ptr<char[]> pAuto(texData);
  •     in.read(texData, size);
  •     in.close();
  •  
  •     uint32_t typeChannel = 0;
  •     if (fmt == GL_BGR)
  •          typeChannel = 3;
  •     assert(typeChannel != 0);
  •  
  •     uint32_t typeSize = 0;
  •     if (type == GL_UNSIGNED_BYTE)
  •          typeSize = 1;
  •     assert(typeSize != 0);
  •  
  •     const int pad = 4;
  •     char* ptr = texData;
  •  
  •     glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  •     for (int i = 0; i < mip; i++)
  •     {
  •          glTexSubImage2D(GL_TEXTURE_2D, i, 0, 0,
  •              width, height, fmt, type, ptr);
  •          ptr += ((width * height * typeChannel * typeSize + (pad - 1)) & (~(pad - 1)));
  •          //assert(ptr <= texData + size);
  •          width /= 2;
  •          if (width == 0)
  •              width = 1;
  •          height /= 2;
  •          if (height == 0)
  •              height = 1;
  •     }
  •  
  •     return tex;
  • }

注意到 glTexStorage2D 函数中有一个 levels 参数,其指定 mip 的层数。之前指定的 levels 参数都是 1。

  • void glTexStorage2D(GLenum target,
  •                     GLsizei levels,
  •                     GLenum internalformat,
  •                     GLsizei width,
  •                     GLsizei height);

使用 glTexSubImage2D 函数传递贴图数据时,此处 level 参数指定需要传递数据给哪个 mip 层。

  • void glTexSubImage2D(GLenum target,
  •                      GLint level,
  •                      GLint xoffset,
  •                      GLint yoffset,
  •                      GLsizei width,
  •                      GLsizei height,
  •                      GLenum format,
  •                      GLenum type,
  •                      const void *pixels);

书中的纹理数据没有额外的对齐操作,需要使用 glPixelStorei 函数。

此例中,除了前面的 mip 数据加载方式有些所差距之外,其他地方之前都接触过。不过这边四个面的生成方式自己还是琢磨了一番,我们先看到代码片段 3.2 所示的顶点着色器。

代码片段 3.2 顶点着色器
  • #version 420 core
  •  
  • out VS_OUT
  • {
  •     vec2 tc;
  • } vs_out;
  •  
  • uniform mat4 mvp;
  • uniform float offset;
  •  
  • void main(void)
  • {
  •     const vec2[4] position = vec2[4](vec2(-0.5, -0.5),
  •                                        vec2( 0.5, -0.5),
  •                                           vec2(-0.5,  0.5),
  •                                           vec2( 0.5,  0.5));
  •  
  •     vs_out.tc = (position[gl_VertexID].xy + vec2(offset, 0.5)) * vec2(30.0, 1.0);
  •     gl_Position = mvp * vec4(position[gl_VertexID], 0.0, 1.0);
  • }

在顶点着色器中,根据 position 可以了解到,在没有进行透视变化之前,四个点是平行于 xy 轴的平面。

代码片段 3.3 计算统一变量
  • vmath::mat4 proj_matrix = vmath::perspective(60.0f,
  •     (float)info.windowWidth / (float)info.windowHeight,
  •     0.1f, 100.0f);
  • glUniform1f(uniforms.offset, t * 0.003f);
  •  
  • int i;
  • GLuint textures[] = { tex_wall, tex_floor, tex_wall, tex_ceiling };
  • for (i = 0; i < 4; i++)
  • {
  •     vmath::mat4 mv_matrix = vmath::rotate(90.0f * (float)i, vmath::vec3(0.0f, 0.0f, 1.0f)) *
  •                             vmath::translate(-0.5f, 0.0f, -10.f) *
  •                              vmath::rotate(90.0f, 0.0f, 1.0f, 0.0f) *
  •                              vmath::scale(30.0f, 1.0f, 1.0f);
  •     vmath::mat4 mvp = proj_matrix * mv_matrix;
  •  
  •     glUniformMatrix4fv(uniforms.mvp, 1, GL_FALSE, mvp);
  •     glBindTexture(GL_TEXTURE_2D, textures[i]);
  •     glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  • }

从代码片段 3.3 中可以看到:首先对这个平行于 xy 轴的平面在 x 轴方向上扩大 30 倍;接着绕 y 轴(向量(0,1,0))旋转 90 度,这样就是墙面的方向了;接着往 x 轴负方向移动 -0.5,往 z 轴负方向移动 -10;最后就是依次绕 z 轴(向量(0,0,1)) 90、180、270 度,生成其余三个面。

4. wrapmodes

这个例子介绍纹理环绕。先把关键代码给出,再结合运行结果能更好的理解各种环绕模式。

代码片段 4.1 计算统一变量
  1. void render(double t)
  2. {
  3.     glClearBufferfv(GL_COLOR, 0, sb7::color::Green);
  4.  
  5.     static const GLenum wrapmodes[] = { GL_CLAMP_TO_EDGE, GL_REPEAT,
  6.          GL_CLAMP_TO_BORDER, GL_MIRRORED_REPEAT};
  7.     static const float offsets[] = { -0.5, -0.5,
  8.                                       0.5, -0.5,
  9.                                      -0.5,  0.5,
  10.                                       0.5,  0.5 };
  11.     glUseProgram(program);
  12.     glViewport(0, 0, info.windowWidth, info.windowHeight);
  13.  
  14.     glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, sb7::color::Yellow);
  15.     for (int i = 0; i < 4; i++)
  16.     {
  17.          glUniform2fv(0, 1, &offsets[i * 2]);
  18.          glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapmodes[i]);
  19.          glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapmodes[i]);
  20.  
  21.          glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  22.     }
  23. }

有四种环绕模式,分别为 GL_CLAMP_TO_EDGE、GL_REPEAT、GL_CLAMP_TO_BORDER、GL_MIRRORED_REPEAT。通过 glTexParameteri 函数,并指定 GL_TEXTURE_WRAP_S 和 GL_TEXTURE_WRAP_T 参数来设置贴图 (s,t) 方向上的环绕模式。

此例的纹理图片如图 5 所示,我们结合图 6 理解上述四种环绕模式:

左上角是 GL_CLAMP_TO_BORDER 模式,如果超出纹理范围的内容则会使用边框的颜色。边框的颜色通过 glTexParameteri 函数,并指定 GL_TEXTURE_BORDER_COLOR 参数来设置。此处边框被设置为黄色。

左下角是 GL_CLAMP_TO_EDGE 模式,如果超出纹理范围则会使用最后一行或者一列的像素。所以此处超出的范围和原本的边框融为了一体。

右下角是 GL_REPEAT 模式,这个模式好理解,之前也用的最多,就是“平铺”的意思。

右上角是 GL_MIRRORED_REPEAT 模式,它在平铺的基础上增加了“镜像”,看效果图片也很好理解。

图5 纹理
图6 运行结果

5. alienrain

这个例子介绍数组纹理。数组纹理顾名思义就是将多个纹理数据打包进数组,从而可以按下标索引纹理数据。为此如代码片段 5.1 所示,这边对先前自己写的纹理加载函数进行了扩充。

代码片段 5.1 自定义加载数组纹理
  1. GLuint TUtils::LoadTexture2DArray(
  2.     const char* texData,
  3.     uint32_t dataSize,
  4.     uint32_t width,
  5.     uint32_t height,
  6.     GLenum internalfmt,
  7.     GLenum fmt,
  8.     GLenum type,
  9.     uint32_t arrSize)
  10. {
  11.     GLuint tex;
  12.     glCreateTextures(GL_TEXTURE_2D_ARRAY, 1, &tex);
  13.     glBindTexture(GL_TEXTURE_2D_ARRAY, tex);
  14.     glTextureStorage3D(tex, 1, internalfmt, width, height, arrSize);
  15.     glTextureSubImage3D(tex, 0, 0, 0, 0, width, height, arrSize, fmt, type, texData);
  16.     return tex;
  17. }
  18.  
  19. GLuint TUtils::LoadTexture(
  20.     std::string&& texPath,
  21.     GLenum target,
  22.     uint32_t width,
  23.     uint32_t height,
  24.     GLenum internalfmt,
  25.     GLenum fmt,
  26.     GLenum type,
  27.     uint32_t arrSize,
  28.     uint32_t mipLevel)
  29. {
  30.     std::ifstream in(texPath, std::ios::binary);
  31.     assert(in.is_open());
  32.     in.seekg(0, std::ios::end);
  33.     uint32_t size = in.tellg();
  34.     in.seekg(0, std::ios::beg);
  35.     char* texData = new char[size];
  36.     std::unique_ptr<char[]> pAuto(texData);
  37.     in.read(texData, size);
  38.     in.close();
  39.  
  40.     GLuint tex = 0;
  41.     if (target == GL_TEXTURE_2D)
  42.          tex = LoadTexture2D(texData, width, height, internalfmt, fmt, type, mipLevel);
  43.     else if (target == GL_TEXTURE_2D_ARRAY)
  44.          tex = LoadTexture2DArray(texData, size, width, height, internalfmt, fmt, type, arrSize);
  45.     else
  46.          assert(0);
  47.  
  48.     return tex;
  49. }

此时针对纹理数组的目标是 GL_TEXTURE_2D_ARRAY。使用 glTextureStorage3D 函数为其分配空间,其中的 depth(z 方向)被当作是数组索引的那个维度。

  • void glTextureStorage3D(GLuint texture,
  •                         GLsizei levels,
  •                         GLenum internalformat,
  •                         GLsizei width,
  •                         GLsizei height,
  •                         GLsizei depth);

使用 glTextureSubImage3D 函数对纹理内容进行传递。总体相关内容见代码片段 5.1 中自己写的 LoadTexture2DArray 函数。

  • void glTextureSubImage3D(GLuint texture,
  •                          GLint level,
  •                          GLint xoffset,
  •                          GLint yoffset,
  •                          GLint zoffset,
  •                          GLsizei width,
  •                          GLsizei height,
  •                          GLsizei depth,
  •                          GLenum format,
  •                          GLenum type,
  •                          const void *pixels);

接着我们看到着色器中是如何索引纹理数组的。如代码片段 5.2 所示,使用 sampler2DArray 统一变量申明纹理数组。并且像素索引 texture 函数也有了 3d 版本的重载,vec3 的前两个分量代表纹理坐标,第三个分量代表数组索引。

代码片段 5.2 片段着色器
  • #version 410 core
  •  
  • layout(location = 0) out vec4 color;
  •  
  • in VS_OUT
  • {
  •     flat int alien;
  •     vec2 tc;
  • } fs_in;
  •  
  • uniform sampler2DArray tex_aliens;
  •  
  • void main(void)
  • {
  •     color = texture(tex_aliens, vec3(fs_in.tc, float(fs_in.alien)));
  • }

其余部分都是顶点数据的传递,这边不做深究。运行结果如图 7 所示,纹理数组的 64 个二维纹理被显示出来。

图7 运行结果

6. fragmentlist

这个例子展示如何在着色器中向纹理写入数据。此例中的纹理已经不是图片数据意义上的纹理了,它将纹理作为一种二维的内存使用,其中存放坐标点对应的链表节点索引。

我们从着色器这部分着手了解,例子中使用了 3 组着色器程序。第一组 clear 着色器,可以理解为链表的初始化操作;第二组 append 着色器,可以理解为向链表插入内容;第三组 resolve 着色器,可以理解为读取链表数据,显示对应内容。

clear

clear 的顶点着色器如代码片段 6.1 所示,可以看到点组成的四边形覆盖整个区域。

代码片段 6.1 clear 顶点着色器
  • #version 430 core
  •  
  • void main(void)
  • {
  •     const vec4 vertices[] = vec4[](vec4(-1.0, -1.0, 0.5, 1.0),
  •                                    vec4( 1.0, -1.0, 0.5, 1.0),
  •                                       vec4(-1.0,  1.0, 0.5, 1.0),
  •                                       vec4( 1.0,  1.0, 0.5, 1.0));
  •     gl_Position = vertices[gl_VertexID];
  • }

clear 片段着色器如代码片段 6.2 所示,注意纹理特意声明了格式(r32ui),采用 uimage2D 变量声明,并且同样是统一变量(uniform)。注意这边使用了 coherent 关键字,资料查的和数据共享的缓冲机制有关,这边暂不清楚。这边用于存储链表的各项地址,为 4 字节整数。

gl_FragCoord.xy 内置变量为渲染点的坐标,以此作为索引初始化链表的初始值。imageStore 函数用于设置纹理数据,这边将初始值设置为 0xFFFFFFFF。

代码片段 6.2 clear 片段着色器
  • #version 430 core
  •  
  • // 2D image to store head pointers
  • layout (binding = 0, r32ui) coherent uniform uimage2D head_pointer;
  •  
  • void main(void)
  • {
  •     ivec2 P = ivec2(gl_FragCoord.xy);
  •  
  •     imageStore(head_pointer, P, uvec4(0xFFFFFFFF));
  • }

append

append 顶点着色器如代码片段 6.3 所示,它的输入是顶点数据块(position),是一个龙的外形。在其基础上,再加上一些变换,这些在之前已经了解过。

代码片段 6.3 append 顶点着色器
  • #version 430 core
  •  
  • layout (location = 0) in vec4 position;
  •  
  • uniform mat4 mvp;
  •  
  • out VS_OUT
  • {
  •     vec4 pos;
  •     vec4 color;
  • } vs_out;
  •  
  • void main(void)
  • {
  •     vec4 p = mvp * position;
  •  
  •     gl_Position = p;
  •     vs_out.color = vec4(1.0);
  •     vs_out.pos = p / p.w;
  • }

append 片段着色器如代码片段 6.4 所示,首先看到缓冲块的定义 list_item_block,其基本结构是 struct list_item,这边简单把它理解为缓冲池。再看到 atomic_uint 原子变量定义 fill_counter,它用于索引“缓冲池”中的位置。

atomicCounterIncrement 是原子操作,将原子变量加一并返回原先的值。这边确保“缓冲池”中的内存不会有冲突的可能。

imageAtomicExchange 也是原子操做,将第三个参数写入对应位置,并返回原先存储的值。这边就是链表连在一起的操作,链接节点(index、old_head)的获取是原子性的,所以不会冲突。这边三维转到二维肯定是有映射到同一个点的,所以这边将相同点的信息用链表方式存储。

代码片段 6.4 append 片段着色器
  • #version 430 core
  •  
  • // Atomic counter for filled size
  • layout (binding = 0, offset = 0) uniform atomic_uint fill_counter;
  •  
  • // 2D image to store head pointers
  • layout (binding = 0, r32ui) coherent uniform uimage2D head_pointer;
  •  
  • // Shader storage buffer containing appended fragments
  • struct list_item
  • {
  •     vec4 color;
  •     float depth;
  •     int facing;
  •     uint next;
  • };
  •  
  • layout (binding = 0, std430) buffer list_item_block
  • {
  •     list_item item[];
  • };
  •  
  • // Input from vertex shader
  • in VS_OUT
  • {
  •     vec4 pos;
  •     vec4 color;
  • } fs_in;
  •  
  • void main(void)
  • {
  •     ivec2 P = ivec2(gl_FragCoord.xy);
  •  
  •     uint index = atomicCounterIncrement(fill_counter);
  •  
  •     uint old_head = imageAtomicExchange(head_pointer, P, index);
  •  
  •     item[index].color = fs_in.color;
  •     item[index].depth = gl_FragCoord.z;
  •     item[index].facing = gl_FrontFacing ? 1 : 0;
  •     item[index].next = old_head;
  • }

这边又一次说明了统一变量:即使是不同的着色器程序,依旧是同一个统一变量。

resolve

resolve 的顶点着色器和 clear 的一样,同样是“全屏幕遍历”。片段着色器如代码片段 6.5 所示,通过 imageLoad 函数从指定位置获取链表内容。根据不同的链表内容,获取计算得到的深度信息,从而输出不同的颜色。

代码片段 6.5 resolve 片段着色器
  • #version 430 core
  •  
  • // 2D image to store head pointers
  • layout (binding = 0, r32ui) coherent uniform uimage2D head_pointer;
  •  
  • // Shader storage buffer containing appended fragments
  • struct list_item
  • {
  •     vec4 color;
  •     float depth;
  •     int facing;
  •     uint next;
  • };
  •  
  • layout (binding = 0, std430) buffer list_item_block
  • {
  •     list_item item[];
  • };
  •  
  • layout (location = 0) out vec4 color;
  •  
  • const uint max_fragments = 10;
  •  
  • void main(void)
  • {
  •     uint frag_count = 0;
  •     float depth_accum = 0.0;
  •     ivec2 P = ivec2(gl_FragCoord.xy);
  •  
  •     uint index = imageLoad(head_pointer, P).x;
  •  
  •     while (index != 0xFFFFFFFF && frag_count < max_fragments)
  •     {
  •          list_item this_item = item[index];
  •  
  •          if (this_item.facing != 0)
  •          {
  •              depth_accum -= this_item.depth;
  •          }
  •          else
  •          {
  •              depth_accum += this_item.depth;
  •          }
  •  
  •          index = this_item.next;
  •          frag_count++;
  •     }
  •  
  •     depth_accum *= 3000.0;
  •     color = vec4(depth_accum, depth_accum, depth_accum, 1.0);
  • }

运行的结果如图 8 所示,可以看到虽然只有黑白的颜色,但是因为不同的深度信息,还是可以分辨出一条烟雾飘渺的龙。

图8 运行结果

总结

6 个关于纹理的例子也算是各种情况的入门:如何创建和传递纹理、什么是纹理坐标、mip 纹理、纹理环绕模式、数据纹理以及如何在着色器中读写纹理。

这篇文章断断续续写了很久,从 21 年写到了 22 年。基础知识部分就差后续一章,相信后面会越学越快。