glActiveTexture和glBindTexture之间的差异和关系

152
据我所了解,glActiveTexture设置活动的“纹理单元”。每个纹理单元可以有多个纹理目标(通常是GL_TEXTURE_1D、2D、3D或CUBE_MAP)。
如果我理解正确,您必须先调用glActiveTexture来设置纹理单元(初始化为GL_TEXTURE0),然后将一个或多个“纹理目标”绑定到该纹理单元?
可用的纹理单元数量取决于系统。在我的库中,我看到最多有32个枚举值。我想这基本上意味着我可以在GPU内存中同时拥有GPU限制(我认为是16 8)和32个纹理?我猜还有一个额外的限制,就是不要超过GPU的最大内存(据说是1 GB)。
我是否正确地理解了纹理目标和纹理单元之间的关系?假设我被允许使用16个单元和4个目标,那么是否有空间容纳16*4=64个目标,或者它不是这样工作的?

接下来,您通常会想要加载纹理。您可以通过glTexImage2D来实现。它的第一个参数是一个纹理目标。如果这个类似于glBufferData, 那么我们实质上将"句柄"/"纹理名称"绑定到纹理目标上,然后将纹理数据加载到该目标中,从而间接地将其与该句柄相关联。

glTexParameter呢?我们必须绑定一个纹理目标,然后再次选择相同的目标作为第一个参数吗?还是只要我们有正确的活动纹理单元,纹理目标就不需要被绑定?

glGenerateMipmap也操作一个目标...为了成功,那个目标仍然必须被绑定到纹理名称上吗?

然后当我们想要在对象上绘制纹理时,我们需要同时选择一个活动纹理单元和一个纹理目标吗?还是我们选择一个纹理单元,然后可以从与该单元关联的4个目标中获取数据?这部分真的让我很困惑。

3个回答

298

OpenGL对象全解析

OpenGL对象的标准模型如下所示。

对象具有状态。可以将它们视为一个struct。因此,您可以定义一个像这样的对象:

struct Object
{
    int count;
    float opacity;
    char *name;
};

这个对象存储了某些值并且有一定的状态。OpenGL对象也有状态。

改变状态

在C/C++中,如果你有一个Object类型的实例,你可以通过以下方式改变它的状态:obj.count = 5; 您可以直接引用对象的实例,获取要更改的特定状态,然后将值插入其中。

但在OpenGL中,您不需要这样做。

由于一些传统原因,为了更改OpenGL对象的状态,必须首先将其与上下文进行绑定。 这是使用某些glBind*调用完成的。

这在C/C++中的等效代码如下:

Object *g_objs[MAX_LOCATIONS] = {NULL};    
void BindObject(int loc, Object *obj)
{
  g_objs[loc] = obj;
}

纹理很有趣; 它们代表绑定的一种特殊情况。许多glBind*函数调用都有一个“目标”参数。这表示OpenGL上下文中可以绑定该类型对象的不同位置。例如,您可以将帧缓冲对象绑定为读取(GL_READ_FRAMEBUFFER)或写入(GL_DRAW_FRAMEBUFFER)。这会影响OpenGL如何使用缓冲区。以上的loc参数就是代表这个。

纹理很特殊,因为当您首次将它们绑定到目标时,它们会获得特殊信息。当您首次将纹理绑定为GL_TEXTURE_2D时,实际上正在设置纹理的特殊状态。您在声明此纹理为2D纹理。并且它将永远是2D纹理;这个状态永远不能改变。如果您有一个首次绑定为GL_TEXTURE_2D的纹理,则必须始终将其绑定为GL_TEXTURE_2D; 试图将其绑定为GL_TEXTURE_1D将会导致错误(在运行时)。

一旦对象被绑定,其状态就可以更改。这通过专用于该对象的通用函数完成。它们也接受代表要修改的对象的位置。

在C / C ++中,代码如下:

void ObjectParameteri(int loc, ObjectParameters eParam, int value)
{
  if(g_objs[loc] == NULL)
    return;

  switch(eParam)
  {
    case OBJECT_COUNT:
      g_objs[loc]->count = value;
      break;
    case OBJECT_OPACITY:
      g_objs[loc]->opacity = (float)value;
      break;
    default:
      //INVALID_ENUM error
      break;
  }
}

请注意这个函数如何设置当前绑定的loc值。

对于纹理对象,主要更改纹理状态的函数是glTexParameter。唯一改变纹理状态的其他函数是glTexImage函数及其变体(glCompressedTexImageglCopyTexImage,最近的glTexStorage)。各种SubImage版本会更改纹理的内容,但它们并不从技术上更改其 stateImage 函数分配纹理存储并设置纹理的格式; SubImage 函数仅复制像素。这不被认为是纹理的状态。

请允许我重申:这些是修改纹理状态的唯一函数。 glTexEnv 修改环境状态; 它不会影响存储在纹理对象中的任何内容。

活动纹理

对于纹理而言,情况更加复杂,这是由于历史原因最好不予公开。这就是glActiveTexture发挥作用的地方。

对于纹理,不仅有目标(GL_TEXTURE_1DGL_TEXTURE_CUBE_MAP等),还有纹理单元。就我们的C / C ++示例而言,我们拥有:

Object *g_objs[MAX_OBJECTS][MAX_LOCATIONS] = {NULL};
int g_currObject = 0;

void BindObject(int loc, Object *obj)
{
  g_objs[g_currObject][loc] = obj;
}

void ActiveObject(int currObject)
{
  g_currObject = currObject;
}

请注意,现在我们不仅拥有一个Object的二维列表,还有一个当前对象的概念。我们有一个设置当前对象的函数,有一个最大当前对象数量的概念,并且我们所有的对象操作函数都被调整为从当前对象中选择。

当您更改当前活动对象时,您会更改整个目标位置集。因此,您可以将某些内容绑定到当前对象0,切换到当前对象4,并修改完全不同的对象。

这个与纹理对象的比喻是完美的...几乎。

看到了吗,glActiveTexture并不接受整数;它接受一个枚举器。理论上,它可以接受从GL_TEXTURE0GL_TEXTURE31的任何东西。但有一件事情你必须要明白:

这是错误的!

glActiveTexture实际可以使用的范围由GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS所规定。这是实现允许的最大同时多重纹理数量。它们被分成不同的组,供不同的着色器阶段使用。例如,在GL 3.x级硬件上,您可以获得16个顶点着色器纹理、16个片段着色器纹理和16个几何着色器纹理。因此,GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS将为48。

但是,不会有48个枚举器。这就是为什么glActiveTexture实际上并不接受枚举器的原因。调用glActiveTexture正确方法如下:

glActiveTexture(GL_TEXTURE0 + i);

其中 i 是介于 0 和 GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS 之间的数字。

渲染

那么所有这些与渲染有什么关系呢?

使用着色器时,您将采样器uniform设置为纹理图像单元(glUniform1i(samplerLoc, i),其中i是图像单元)。这代表您与glActiveTexture一起使用的数字。采样器将根据采样器类型选择目标。因此,sampler2D将从GL_TEXTURE_2D目标中选择。这就是采样器具有不同类型的原因之一。

现在这听起来很像您可以有两个GLSL采样器,具有不同的类型,并且使用相同的纹理图像单元。但实际上你不能这样做;OpenGL禁止这样做,并会在尝试渲染时给出错误提示。


14
哇!又一个精彩的回答-谢谢Nicol!我特别喜欢那段关于2D纹理始终是2D纹理的内容。我现在正在构建一些包装器,不确定是否应该让其开放以进行更改。还有GL_TEXTURE0 + i的部分-我想检查枚举值以确定是否有效。最后一段-不知道是否合法。太棒了!我会将您所有的回答收藏起来,以便今后参考。 - mpen
7
@Nicol Bolas: 这解释得很好。你应该将其中一些内容复制到你的在线opengl书籍的贴图章节中。我认为这样更加清晰,可以很好地补充这一章节。 - WesDec
4
@Nicol Bolas 我刚开始学习OpenGL,这个回答对我帮助很大。谢谢您! - inline
2
嘿,尼科,我只是想指出你的小错误:应该是GL_DRAW_FRAMEBUFFER而不是GL_WRITE_FRAMEBUFFER。 - Defd
3
@Nicol: 哇,此前我对这个的最佳定义是来自您的arcsynthesis教程,现在您甚至超越了那个出色的来源。谢谢你。 - Baggers
显示剩余5条评论

23

我会尝试!所有这些都不是很复杂,只是一个术语的问题,希望我能让自己表达清楚。


您可以创建大约与系统中可用内存一样多的纹理对象。这些对象保存纹理的实际数据(texels),以及由glTexParameter提供的参数(请参见FAQ)。
在创建时,您必须将一个纹理目标分配给一个纹理对象,该目标表示纹理的类型(GL_TEXTURE_2D,GL_TEXTURE_3D,GL_TEXTURE_CUBE等)。
这两个项目,纹理对象和纹理目标代表纹理数据。稍后我们会回到它们。
纹理单元
现在,OpenGL提供了一组纹理单元,可以在绘制时同时使用。数组的大小取决于OpenGL系统,您的系统有8个。
您可以将纹理对象绑定到纹理单元,以在绘制时使用给定的纹理。
在一个简单易懂的世界里,要使用给定的纹理进行绘制,你需要将纹理对象绑定到纹理单元上,并执行以下伪代码:
glTextureUnit[0] = textureObject

作为一个状态机,GL不幸的是不能这样工作。假设我们的textureObject有GL_TEXTURE_2D纹理目标的数据,我们将前面的赋值表达为:
glActiveTexture(GL_TEXTURE0);                   // select slot 0 of the texture units array
glBindTexture(GL_TEXTURE_2D, textureObject);    // do the binding

请注意,GL_TEXTURE_2D 真正取决于你想要绑定的纹理类型。
纹理对象
在伪代码中,要设置纹理数据或纹理参数,例如:
setTexData(textureObject, ...)
setTexParameter(textureObject, TEXTURE_MIN_FILTER, LINEAR)

OpenGL不能直接操作纹理对象,要更新/设置它们的内容或更改它们的参数,必须首先将它们绑定到活动纹理单元(无论是哪个)。等效的代码如下:

glBindTexture(GL_TEXTURE_2D, textureObject)       // this 'installs' textureObject in texture unit
glTexImage2D(GL_TEXTURE_2D, ...)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)

着色器

着色器可以访问所有的纹理单元,它们不关心活动纹理。

采样器统一变量是表示要用于采样器的纹理单元索引的int值(而不是要使用的纹理对象)。

因此,您需要将纹理对象绑定到想要使用的单元。

采样器的类型将与在纹理单元中使用的纹理目标进行匹配:GL_TEXTURE_2D使用Sampler2D等。


有一件事我不太明白。假设我有一些纹理,并且它在许多着色器中使用,位于不同的纹理单元上。假设我想在运行时更改纹理过滤方式。我应该使用哪个纹理单元?我能否在 Unit 0 上更改纹理状态,然后在不同的单元上使用该纹理? - majakthecoder
在我的回答中,我认为过滤是纹理对象的属性 - 这意味着您无法在特定的纹理单元中更改它。根据您的OpenGL版本,您可以使用采样器对象来解决此问题(https://www.opengl.org/wiki/Sampler_Object),否则,您可能需要复制纹理对象以进行多个同时过滤。 - rotoglup

12

想象GPU就像一家油漆加工厂。

有许多储罐,它们将染料输送到某个喷漆机中。在喷漆机中,染料被应用于物体上。这些储罐是纹理单元

这些储罐可以装备不同种类的染料。每种染料都需要其他种类的溶剂。"溶剂"就是纹理目标。为了方便,每个储罐都连接到某个溶剂供应器,但每个储罐一次只能使用一种溶剂。因此,有一个阀门/开关TEXTURE_CUBE_MAPTEXTURE_3DTEXTURE_2DTEXTURE_1D。你可以同时把所有染料类型注入储罐,但由于只有一种溶剂进入,它只会稀释匹配的染料种类。因此,你可以绑定每种纹理,但与"最重要"的溶剂绑定实际上会混入储罐并与其所属的染料种类混合。

然后是染料本身,它来自仓库,并通过“绑定”将其装满储罐。这就是你的纹理。


2
有点奇怪的比喻...我不确定它是否真正解决了任何问题。特别是“稀释”和“最重要的溶剂”部分。你是说如果我绑定了2D纹理和3D纹理,我只能使用其中一个,还是怎样?哪个被认为是最重要的? Translated text: 有点奇怪的比喻...我不确定它是否真正解决了任何问题。特别是“稀释”和“最重要的溶剂”部分。你是说如果我绑定了2D纹理和3D纹理,我只能使用其中一个,还是怎样?哪个被认为是最重要的? - mpen
2
@Mark:嗯,我试图用绘画师使用字面染料(比如油性和水性)的术语来表达。无论如何,如果您绑定并启用多个纹理目标,则有优先级:CUBE_MAP > 3D > TEXTURE_ARRAY > 2D > 1D。 - datenwolf
1
太好了!我不知道有这个优先级。现在我知道每个纹理单元只能使用一个纹理目标,这更有意义了。 - mpen
1
@legends2k:现在变得有趣了。我们是在谈论核心还是兼容性配置文件呢?我们假设理想的还是有缺陷的驱动程序。理论上,uniform 的类型选择哪个纹理单元的目标进行选择。在实践中,这发生在核心配置文件中。在兼容性配置文件中,如果纹理单元的先前目标与采样器的类型不匹配,那么预计会出现一些有缺陷的驱动程序向您呈现全白色的默认纹理。 - datenwolf
1
@legends2k: 另外,请考虑一下如果将2D和3D纹理绑定到同一个单元,然后你又有一个绑定到同一单元的2D和3D采样器统一变量会发生什么情况?这可能会触发各种奇怪的驱动程序错误。在实践中,思考旧的固定功能优先模型可以让你的头脑保持清醒,使你的程序正常运行,因为大多数驱动程序都会以可预测的方式表现出来。 - datenwolf
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接