《OGL dev》Etay Meiri Tutorial 16 - Basic Texture Mapping 笔记

纹理映射的流程:

  1. 将纹理加载到OpenGL。
  2. 提供与顶点一起的纹理坐标。
  3. 光栅化三角形时,对纹理坐标进行插值。
  4. 在fragment shader中,开发者依据插值后的纹理坐标进行采样,得到texel(纹素)。
    • texel是纹理中的一像素,包含一个颜色。

虽然我们只使用2D纹理,但OpenGL支持多种纹理: 1D、2D、3D、cube等。
2D纹理,宽乘以高就是texel的数量。

纹理坐标是纹理空间中的坐标,纹理空间如图:

纹理坐标会“粘”在顶点上,在顶点变换后,保持不变。也存在改变纹理坐标的技术,但现在我们不涉及。

filtering(过滤)是纹理映射中非常重要的概念。因为texel在纹理中的位置总是整数,而纹理坐标映射到纹理中的位置却很可能是浮点数,需要一种方案将浮点数转换为整数,这样才能得到texel。这个方案称为filtering。

filtering有很多种,例如:

  1. nearest filtering(最近过滤),又称为point filtering(点过滤):对浮点数位置进行四舍五入。
  2. linear filtering(线性过滤):取得浮点数位置附近的四个整数位置的texel,对它们的颜色进行插值,得到浮点数位置的颜色。
    • 例如(152.34,745.14),就取(152,745)、(153,745)、(152,744)和(153,744) 四个texel进行插值。因为小数部分为(0.34, 0.14),最终颜色会更接近(152,745)。
  • 效果更好的filtering通常需要消耗GPU更多性能。

OpenGL中通过纹理对象、texture unit(纹理单元)、sampler object和sampler uniform这四个概念,实现纹理映射。

纹理对象的数据是texel,类型可以是1D、2D等,底层数据类型可以是RGB、RGBA等。
纹理对象必须指定一个数据源,以加载纹理。
纹理对象可以设置filtering类型。
纹理对象关联一个handle,可以通过动态绑定handle到OpenGL state来切换纹理。由OpenGL保证在渲染之前加载纹理到GPU。

纹理对象与shader无法直接交互,必须通过texture unit。
texture unit通常有很多个,具体数量取决于显卡的能力。
先激活某个texture unit,然后将纹理对象绑定到这个texture unit,之后shader可以通过texture unit的索引访问到绑定的纹理对象。
一个纹理对象可以绑定到多个texture unit。

一个texture unit包含多个类型不同的target,target的类型分为1D、2D等。
一个纹理对象绑定到texture unit,实际上是绑定到这个texture unit中某个类型的target。
因此,一个texture unit可以绑定多个的纹理对象,每个绑定到不同类型的target。

  • 教程没有说明纹理对象与target的类型是否必须相同。

采样通常发生在fragment shader中,一个特殊的函数执行这个操作。它需要知道对哪个texture unit的哪个target进行采样,sampler uniform variable负责这件事。
sampler uniform variable从应用接收一个整数作为texture unit的索引。sampler uniform variable的类型说明target的类型,可以是sampler1D、sampler2D、sampler3D、samplerCube等。
可以存在多个sampler uniform variable对应同一个texture unit。

sampler object和纹理对象一样都可以绑定到texture unit。
sampler object和纹理对象一样都可以设置采样操作的参数,这是sampling state的一部分。
当一个texture unit同时绑定了sampler object和纹理对象,则sampler object的设置sampling state会覆盖纹理对象的设置。
现在我们不使用sampler object。

这些概念之间的关系如下图:

glGenTextures(1, &m_textureObj);,创建纹理对象。与glGenBuffers()类似,第一个参数是要创建的纹理对象数量。第二个参数是GLuint数组指针,用于存放handle。

glBindTexture(m_textureTarget, m_textureObj);,将一个纹理对象绑定到OpenGL,在绑定新的纹理对象之前,OpenGL所有纹理相关的调用都引用这个纹理对象。绑定时纹理对象,还要指定绑定到的texture target的类型,可以是GL_TEXTURE_1D、GL_TEXTURE_2D等。

  • 第二个参数为0,表示使用默认的纹理。

glTexImage2D(m_textureTarget, 0, GL_RGBA, m_pImage->columns(), m_pImage->rows(), 0, GL_RGBA, GL_UNSIGNED_BYTE, m_blob.data());,从内存中加载纹理数据到纹理对象。

  1. 第二个参数是LOD(Level-Of-Detail)。一个纹理对象可以包含不同分辨率的相同纹理,称为mip-mapping。每一个mip-map有不同的LOD索引,从0开始,0表示最高的分辨率,随分辨率减少增加。因为我们只有一个mip-map,所以使用0。
  2. 第三个参数是在OpenGL中存储纹理数据的格式,可以是1-4的数字,或GL开头的枚举,如GL_RED、GL_RGBA。经测试1和2会显示黑白纹理,3和4会显示完整的纹理,GL_RED只显示红色通道的纹理,GL_RGBA显示完整的纹理。
  3. 之后两个参数是以texel为单位的纹理的宽高。
  4. 第六个参数是边界,暂时我们只使用0。
  5. 倒数第三个参数说明内存中纹理数据的格式。
  6. 倒数第二个参数说明每个通道占的内存大小,GL_UNSIGNED_BYTE表示1 byte。
  7. 最后一个参数是内存中纹理数据的数据源。(加载纹理之后就可以释放)
glTexParameterf(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

glTexParameterf可以设置一部分sampling state。这里设置缩小和放大都使用linear filtering,因为根据与摄影机距离的不同,纹理会被缩放。

void Texture::Bind(GLenum TextureUnit)
{
    glActiveTexture(TextureUnit);
    glBindTexture(m_textureTarget, m_textureObj);
}

glActiveTexture()激活texture unit,texture unit可以是GL_TEXTURE0、GL_TEXTURE1等枚举(不能使用索引)。
glBindTexture()将纹理对象绑定到激活的texture unit。

FragColor = texture2D(gSampler, TexCoord0.st);

内置texture2D函数采样纹理。

  1. 第一个参数,gSampler是sampler2D类型的sampler uniform variable,值为texture unit的索引。
  2. 第二个参数是纹理坐标。
  3. 返回值是经过filtering的采样的texel。
...
glEnableVertexAttribArray(1);
...
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)12);
...
pTexture->Bind(GL_TEXTURE0);
...
glDisableVertexAttribArray(1);

glVertexAttribPointer()、glVertexAttribPointer()、glDisableVertexAttribArray()的第一个参数1,对应vertex shader中的layout (location = 1) in vec2 TexCoord;,用于纹理坐标。
第二个glVertexAttribPointer()说明纹理坐标在vertex buffer中的位置。

  1. 倒数第二个参数表示一个vertex attribute到下一个vertex相同的attribute之间的byte数,称为vertex stride。如果只有一个attribute,vertex stride可以设置为0。
  2. 最后一个参数是vertex buffer首地址开始到第一个attribute的byte数,类型必须强转为GLvoid*,这是这个函数所期望的。
glFrontFace(GL_CW);
glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);

背面剔除默认是禁用的,以下启用背面剔除:

  1. glFrontFace(GL_CW);,设定顺时针为正面。
  2. glCullFace(GL_BACK);,设定剔除背面。
  3. glEnable(GL_CULL_FACE);,启动背面剔除。

glUniform1i(gSampler, 0);,将texture unit的索引传递到sampler uniform variable中。(注意不能使用枚举)