在这一篇文章中,我们开始介绍纹理。在此之前,我们也有一个小铺垫:在 《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 的统一变量中。此时输出的颜色从纹理中获取,代码中展示了可以通过 texelFetch 或 texture 函数获取。
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::ktx 和 sb7::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 使用自定义函数
//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);
图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 计算统一变量
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 纹理
图6 运行结果
5. alienrain
这个例子介绍数组纹理。数组纹理顾名思义就是将多个纹理数据打包进数组,从而可以按下标索引纹理数据。为此如代码片段 5.1 所示,这边对先前自己写的纹理加载函数进行了扩充。
代码片段 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 的前两个分量代表纹理坐标,第三个分量代表数组索引。
代码片段 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 年。基础知识部分就差后续一章,相信后面会越学越快。