数据 - 纹理
在这一篇文章中,我们开始介绍纹理。在此之前,我们也有一个小铺垫:在 《KTX 格式和 SBM 格式》 中,我们可以大致了解书中使用的一种纹理数据封装格式。
书中关于纹理的示例非常多,这非常好,相关的知识点会更加直观。之后的内容也把示例代码作为组织顺序,记录各个知识点。
1. simpletexture
如代码片段 1.1 所示,设置纹理相关的内容,和设置缓冲相关内容很相似。glCreateTextures 创建一个纹理名称。glBindTexture 将纹理和绑定点绑定,以便在着色器中使用。glTexStorage2D 分配存储纹理的空间。glTexSubImage2D 传递纹理数据。
- 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 纹理的样子。
- 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
- }

接下来看着色器部分的代码。此代码示例使用了顶点着色器和片段着色器,顶点着色器是硬编码的三角形的 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 的统一变量中。此时输出的颜色从纹理中获取,代码中展示了可以通过 texelFetch 或 texture 函数获取。
texelFetch 的第一个参数指定纹理统一变量,第二参数指定纹理坐标,第三个参数指定 mip 图层。需要注意的是 texelFetch 对应的纹理坐标是整型,和图片的长宽尺寸一一对应。此处如果使用 texelFetch 函数,则会出现三角形只有一小部分有贴图的现象。因为纹理图片像素很小,只有 256x256,而窗体的像素很大。
texture 的第一个参数指定纹理统一变量,第二参数指定纹理坐标。需要注意的是 texture 对应的纹理坐标是浮点型,范围为 0 到 1,所以这边除以纹理图片的大小进行简单的归一化。而纹理的大小通过 textureSize 函数获取,第一个参数指定纹理变量,第二个参数指定 mip 层。
图 2 是此示例运行的结果,可以在图中看到图 1 贴图的“影子”。

纹理坐标实验
以上纹理是如何映射的细节没有琢磨,同时书中此章的示例中都没有介绍纹理坐标的概念。在此稍微补充一下。
看到代码片段 1.4,其中定义了两个三角形:右下角一个、左上角一个。前面已经讲过 texture 指定的纹理坐标是 0 到 1,即将图片的宽和高按比例放在 (0,1) 范围内。我们将每个顶点映射到图片上的坐标点,OpenGL 就会“自动绘制”这个区域的图片颜色。
- #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 轴上的坐标“镜像”一下。

我想书中没讲解纹理坐标映射应该是有琐碎之处(因为我目前也不太明白图片为什么会反过来😂)。
这些暂不影响 OpenGL 的原理学习,把纹理坐标先交给 Maya 这些现成的软件吧!
我发现类 generate_texture 的函数有妙用:可以检查是读取的纹理有问题,还是 glTexStorage2D 等函数的参数设置有问题。
2. simpletexcoords
这个例子是我们首次接触到书籍作者的 sb7::ktx 和 sb7::object 库。sb7::ktx 用于加载纹理数据,此例中加载逗号符号状的纹理;sb7::object 用于加载顶点数据,此例中加载甜甜圈状的模型。
正如在 《KTX 格式和 SBM 格式》 中所说的,调用封装的库就会有一种“自己什么都没做”、“不知道怎么就可以了”的感觉。我们借此例来了解这两个库,了解的方式是采用之前学习的内容自己实现一遍。因此原书的代码在这边就不贴出来了。
首先我们先看自己写的纹理加载函数。如代码片段 2.1 所示,虽然参数穿的有点多,但是流程非常简单,我们在第一个 simpletexture 例子中已经学习过了:glCreateTextures 创建纹理;glBindTexture 绑定纹理用于后续操作;glTexStorage2D 分配纹理空间;glTexSubImage2D 传递纹理数据。
- 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 进一步指定着色器中的顶点属性的属性。
- 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 文件中的布局情况。
- //tex_object[1] = sb7::ktx::file::load("D:\\OpenGL\\superbible7-media\\textures\\pattern1.ktx");
- tex_object[1] = TUtils::LoadTexture("pattern1_1024x1024_RGBA8888.rgb", 1024, 1024, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE);
- //object.load("D:\\OpenGL\\superbible7-media\\objects\\torus_nrms_tc.sbm");
- glCreateVertexArrays(1, &vao);
- glBindVertexArray(vao);
- buffer[0] = TUtils::LoadVAOAttrib(vao, "torus_nrms_tc_Position(12288).bin", 4, GL_FLOAT, 0, 0, &vexCnt);
- buffer[1] = TUtils::LoadVAOAttrib(vao, "torus_nrms_tc_Map(12288).bin", 2, GL_FLOAT, 4, 1, &vexCnt);

3. tunnel
这个例子展示一个隧道,介绍 mip 贴图。mip 贴图用于解决闪烁(混叠伪影)的效果。我们看到视频 1,当我们没有使用 mip 贴图时(远近都使用同一张贴图时),尤其当物体移动时,会产生波光粼粼的闪烁现象。
mip 映射纹理在贴图层面上的概念非常简单,就是提供不同大小尺寸的贴图:每个图像在上一张图片的宽高上缩小 1/2。我们简单的将各种分辨率的图片传递给 OpenGL,就能享受到这种强大的纹理技术。
传递的方法我们之前也学习过了,之前和纹理相关的函数中会有跟 mip 相关的参数。当时不了解,现在就可以了解了。我们借自己改进的纹理加载函数,再次学习原先 OpenGL 函数中关于 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 所示的顶点着色器。
- #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 轴的平面。
- 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
这个例子介绍纹理环绕。先把关键代码给出,再结合运行结果能更好的理解各种环绕模式。
- void render(double t)
- {
- glClearBufferfv(GL_COLOR, 0, sb7::color::Green);
- static const GLenum wrapmodes[] = { GL_CLAMP_TO_EDGE, GL_REPEAT,
- GL_CLAMP_TO_BORDER, GL_MIRRORED_REPEAT};
- static const float offsets[] = { -0.5, -0.5,
- 0.5, -0.5,
- -0.5, 0.5,
- 0.5, 0.5 };
- glUseProgram(program);
- glViewport(0, 0, info.windowWidth, info.windowHeight);
- glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, sb7::color::Yellow);
- for (int i = 0; i < 4; i++)
- {
- glUniform2fv(0, 1, &offsets[i * 2]);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapmodes[i]);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapmodes[i]);
- glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
- }
- }
有四种环绕模式,分别为 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. alienrain
这个例子介绍数组纹理。数组纹理顾名思义就是将多个纹理数据打包进数组,从而可以按下标索引纹理数据。为此如代码片段 5.1 所示,这边对先前自己写的纹理加载函数进行了扩充。
- GLuint TUtils::LoadTexture2DArray(
- const char* texData,
- uint32_t dataSize,
- uint32_t width,
- uint32_t height,
- GLenum internalfmt,
- GLenum fmt,
- GLenum type,
- uint32_t arrSize)
- {
- GLuint tex;
- glCreateTextures(GL_TEXTURE_2D_ARRAY, 1, &tex);
- glBindTexture(GL_TEXTURE_2D_ARRAY, tex);
- glTextureStorage3D(tex, 1, internalfmt, width, height, arrSize);
- glTextureSubImage3D(tex, 0, 0, 0, 0, width, height, arrSize, fmt, type, texData);
- return tex;
- }
- GLuint TUtils::LoadTexture(
- std::string&& texPath,
- GLenum target,
- uint32_t width,
- uint32_t height,
- GLenum internalfmt,
- GLenum fmt,
- GLenum type,
- uint32_t arrSize,
- uint32_t mipLevel)
- {
- 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();
- GLuint tex = 0;
- if (target == GL_TEXTURE_2D)
- tex = LoadTexture2D(texData, width, height, internalfmt, fmt, type, mipLevel);
- else if (target == GL_TEXTURE_2D_ARRAY)
- tex = LoadTexture2DArray(texData, size, width, height, internalfmt, fmt, type, arrSize);
- else
- assert(0);
- return tex;
- }
此时针对纹理数组的目标是 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 的前两个分量代表纹理坐标,第三个分量代表数组索引。
- #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 个二维纹理被显示出来。

6. fragmentlist
这个例子展示如何在着色器中向纹理写入数据。此例中的纹理已经不是图片数据意义上的纹理了,它将纹理作为一种二维的内存使用,其中存放坐标点对应的链表节点索引。
我们从着色器这部分着手了解,例子中使用了 3 组着色器程序。第一组 clear 着色器,可以理解为链表的初始化操作;第二组 append 着色器,可以理解为向链表插入内容;第三组 resolve 着色器,可以理解为读取链表数据,显示对应内容。
clear
clear 的顶点着色器如代码片段 6.1 所示,可以看到点组成的四边形覆盖整个区域。
- #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。
- #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),是一个龙的外形。在其基础上,再加上一些变换,这些在之前已经了解过。
- #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)的获取是原子性的,所以不会冲突。这边三维转到二维肯定是有映射到同一个点的,所以这边将相同点的信息用链表方式存储。
- #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 函数从指定位置获取链表内容。根据不同的链表内容,获取计算得到的深度信息,从而输出不同的颜色。
- #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 所示,可以看到虽然只有黑白的颜色,但是因为不同的深度信息,还是可以分辨出一条烟雾飘渺的龙。

总结
6 个关于纹理的例子也算是各种情况的入门:如何创建和传递纹理、什么是纹理坐标、mip 纹理、纹理环绕模式、数据纹理以及如何在着色器中读写纹理。
这篇文章断断续续写了很久,从 21 年写到了 22 年。基础知识部分就差后续一章,相信后面会越学越快。