GLSL Texture2D vs Sampler in Separate Pairs

Modern graphics APIs like Vulkan and modern OpenGL allow developers to decouple texture data from the state used to read it. In GLSL, this decoupling is represented by separating raw texture objects, such as texture2D, from pure sampler objects, designated as sampler. Understanding the difference between these types is fundamental to optimizing resource usage, descriptor management, and shader flexibility in modern rendering pipelines.

The Role of texture2D

The texture2D type (and related types like textureCube or texture3D) represents the underlying image resource. It acts purely as a handle to the texture's data and its memory allocation.

Key characteristics of texture2D include:

  • Data Storage: It holds the actual texel data, dimensions, mipmap levels, and format information (e.g., RGBA8, R32F).
  • No Filtering Logic: It contains no information on how coordinates should wrap, whether texels should be interpolated using nearest-neighbor or bilinear filtering, or how level-of-detail (LOD) transitions should be computed.
  • Direct Access: You can read directly from a texture2D without a sampler using texel fetching functions like texelFetch(), which require exact integer pixel coordinates rather than normalized floating-point UV coordinates.

The Role of sampler

The sampler type represents the state and algorithms used to read and filter image data. It contains no pixel data, dimensions, or memory buffers of its own.

Key characteristics of sampler include:

  • Sampling State: It encapsulates parameters such as minification and magnification filtering (nearest, linear), mipmap filtering modes, coordinate wrapping modes (clamp-to-edge, repeat, mirrored repeat), border colors, and anisotropic filtering settings.
  • Reusability: A single sampler state can be bound once and reused across hundreds of different textures, ensuring consistent sampling behavior without duplicating state data.

How They Combine in GLSL

In legacy GLSL, a sampler2D was an opaque uniform that inherently combined the texture data and the sampler state into a single object. When using separate image-sampler pairs (commonly enabled via Vulkan GLSL or OpenGL extensions like GL_ARB_gl_spirv), you declare the texture and sampler independently:

layout(set = 0, binding = 0) uniform texture2D uTexture;
layout(set = 0, binding = 1) uniform sampler uSampler;

To sample from uTexture using normalized coordinates and filtering, you combine them on the fly into a combined image-sampler constructor:

vec4 color = texture(sampler2D(uTexture, uSampler), uv);

Key Differences and Advantages

  • Decoupled Architecture: texture2D is the data payload, while sampler is the access rulebook. Separating them mirrors how modern GPU hardware operates under APIs like Vulkan, DirectX 12, and Metal.
  • Descriptor Efficiency: Modern GPUs have hard limits on the number of active sampler states (often 16 to 32), while supporting thousands of bound textures. Decoupling them allows \(N\) textures to be paired with \(M\) samplers (\(N \times M\) combinations) without wasting descriptor slots on redundant, identical sampler states.
  • Shader Flexibility: The same texture can be sampled in the same shader pass using multiple methods—for instance, using a bilinear sampler for general rendering and a point-filtered sampler for pixel-aligned processing—without rebinding or duplicating the texture resource.