Combined Image Samplers vs Separate Images in Vulkan GLSL

Vulkan GLSL supports two distinct approaches for sampling textures: combined image samplers and decoupled (separate) sampled images paired with standalone samplers. While combined image samplers bundle the texture data and sampling configuration into a single descriptor, the separated approach decouples the image resource from the sampler state. This article explains the syntax used to declare both types in GLSL, how they map to Vulkan descriptor types, and how to differentiate their usage in shader code.

Combined Image Samplers

A combined image sampler packages an image resource and a sampler configuration into a single entity. It corresponds to the Vulkan descriptor type VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER.

Declaration Syntax

To declare a combined image sampler, use the traditional OpenGL-style sampler types (such as sampler2D, samplerCube, or sampler3D):

layout(set = 0, binding = 0) uniform sampler2D combinedImageSampler;

Sampling in Shader Code

Because the sampling parameters (filtering, addressing modes) and the image data are coupled, you can pass the identifier directly to GLSL texture lookup functions:

vec4 color = texture(combinedImageSampler, uvCoordinates);

Separate Sampled Images and Samplers

Vulkan allows the separation of image view state from sampler state. This corresponds to two distinct descriptor types: VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE for the texture data and VK_DESCRIPTOR_TYPE_SAMPLER for the filtering and wrapping configurations. Decoupling them allows a single sampler to be reused across multiple textures, or multiple samplers (such as nearest and linear) to sample the same texture.

Declaration Syntax

Separate resources are declared using the texture prefix for image resources and the raw sampler keyword for sampler objects:

layout(set = 0, binding = 0) uniform texture2D separateTexture;
layout(set = 0, binding = 1) uniform sampler separateSampler;
  • texture2D (or textureCube, texture3D, etc.) represents only the image data.
  • sampler represents only the sampling rules (filtering, mipmap modes, wrapping).

Sampling in Shader Code

Because a texture2D cannot be read without specifying sampling behavior, you must construct a temporary combined sampler at the point of sampling using the constructor syntax:

vec4 color = texture(sampler2D(separateTexture, separateSampler), uvCoordinates);

The expression sampler2D(separateTexture, separateSampler) combines the two separate components on the fly for the texture() call.

Distinguishing Between the Two Approaches

You can easily distinguish combined image samplers from separate images by examining three primary areas:

  1. Type Names in GLSL Declarations
    • Combined: Prefixed with sampler followed by the dimensional type (e.g., sampler2D, sampler2DArray, samplerCube).
    • Separate: Images are declared using texture followed by dimension (e.g., texture2D, textureCube), while samplers are declared using the pure keyword sampler.
  2. Descriptor Set Allocation
    • Combined image samplers consume a single descriptor binding (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER).
    • Separate images and samplers require two separate bindings: one for VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE and another for VK_DESCRIPTOR_TYPE_SAMPLER.
  3. Sampling Call Syntax
    • Combined: Evaluated directly: texture(mySampler2D, uv).
    • Separate: Evaluated via constructor syntax: texture(sampler2D(myTexture2D, mySampler), uv).