Texture mapping/Combined image sampler

目录

Introduction

Updating the descriptors

Texture coordinates

Shaders


Introduction

我们第一次在教程的统一缓冲区部分查看了描述符。在本章中,我们将研究一种新的描述符:组合图像采样器。此描述符使着色器可以通过采样器对象访问图像资源,就像我们在上一章中创建的那样。

我们将首先修改描述符布局、描述符池和描述符集,以包括这样一个组合的图像采样器描述符。之后,我们将向“顶点”添加纹理坐标,并修改片段着色器以从纹理中读取颜色,而不仅仅是插值顶点颜色。

Updating the descriptors

浏览到createDescriptorSetLayout函数,并为组合图像采样器描述符添加VkDescriptorSetLayoutBinding。我们只需将其放在统一缓冲区之后的绑定中:

VkDescriptorSetLayoutBinding samplerLayoutBinding{};
samplerLayoutBinding.binding = 1;
samplerLayoutBinding.descriptorCount = 1;
samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
samplerLayoutBinding.pImmutableSamplers = nullptr;
samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;

std::array<VkDescriptorSetLayoutBinding, 2> bindings = {uboLayoutBinding, samplerLayoutBinding};
VkDescriptorSetLayoutCreateInfo layoutInfo{};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
layoutInfo.pBindings = bindings.data();

确保设置stageFlags以指示我们打算在片段着色器中使用组合图像采样器描述符。这就是决定片段颜色的地方。可以在顶点着色器中使用纹理采样,例如通过高度贴图动态变形顶点网格。

我们还必须通过向VkDescriptorPoolCreateInfo添加另一个VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER类型的VkPoolSize来创建更大的描述符池,为组合图像采样器的分配腾出空间。转到createDescriptorPool函数并将其修改为包含此描述符的VkDescriptorPoolSize

std::array<VkDescriptorPoolSize, 2> poolSizes{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSizes[0].descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[1].descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);

VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
poolInfo.pPoolSizes = poolSizes.data();
poolInfo.maxSets = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);

描述符池不足是验证层无法捕获的问题的一个很好的例子:从Vulkan 1.1开始,如果池不够大,vkAllocateDescriptorSet可能会失败,错误代码为VK_ERROR_POOL_OUT_OF_MEMORY,但驱动程序也可能会尝试在内部解决问题。这意味着有时(取决于硬件、池大小和分配大小),驱动程序会让我们摆脱超出描述符池限制的分配。其他时候,vkAllocateDescriptorSet将失败并返回VK_ERROR_POOL_OUT_OF_MEMORY。如果在某些机器上分配成功,但在其他机器上分配失败,这可能会特别令人沮丧。

由于Vulkan将分配的责任转移到驱动程序,因此不再严格要求只分配由相应descriptorCount成员指定的特定类型的描述符(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER等),以创建描述符池。然而,这样做仍然是最佳实践,并且在未来,如果启用最佳实践验证,VK_LAYER_KHRONOS_validation将警告此类问题。

最后一步是将实际图像和采样器资源绑定到描述符集中的描述符。转到createDescriptorSets函数。

for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
    VkDescriptorBufferInfo bufferInfo{};
    bufferInfo.buffer = uniformBuffers[i];
    bufferInfo.offset = 0;
    bufferInfo.range = sizeof(UniformBufferObject);

    VkDescriptorImageInfo imageInfo{};
    imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    imageInfo.imageView = textureImageView;
    imageInfo.sampler = textureSampler;

    ...
}

必须在VkDescriptorImageInfo结构中指定组合图像采样器结构的资源,就像在VkDescriptorBufferInfo描述符缓冲信息结构中指定统一缓冲描述符的缓冲区资源一样。这是上一章中的对象汇集在一起的地方。

std::array<VkWriteDescriptorSet, 2> descriptorWrites{};

descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[0].dstSet = descriptorSets[i];
descriptorWrites[0].dstBinding = 0;
descriptorWrites[0].dstArrayElement = 0;
descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrites[0].descriptorCount = 1;
descriptorWrites[0].pBufferInfo = &bufferInfo;

descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[1].dstSet = descriptorSets[i];
descriptorWrites[1].dstBinding = 1;
descriptorWrites[1].dstArrayElement = 0;
descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptorWrites[1].descriptorCount = 1;
descriptorWrites[1].pImageInfo = &imageInfo;

vkUpdateDescriptorSets(device, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);

描述符必须使用此图像信息进行更新,就像缓冲区一样。这次我们使用的是pImageInfo数组而不是pBufferInfo。描述符现在可以被着色器使用了!

Texture coordinates

纹理贴图还有一个重要的要素仍然缺失,那就是每个顶点的实际坐标。坐标决定图像实际映射到几何体的方式。

struct Vertex {
    glm::vec2 pos;
    glm::vec3 color;
    glm::vec2 texCoord;

    static VkVertexInputBindingDescription getBindingDescription() {
        VkVertexInputBindingDescription bindingDescription{};
        bindingDescription.binding = 0;
        bindingDescription.stride = sizeof(Vertex);
        bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

        return bindingDescription;
    }

    static std::array<VkVertexInputAttributeDescription, 3> getAttributeDescriptions() {
        std::array<VkVertexInputAttributeDescription, 3> attributeDescriptions{};

        attributeDescriptions[0].binding = 0;
        attributeDescriptions[0].location = 0;
        attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT;
        attributeDescriptions[0].offset = offsetof(Vertex, pos);

        attributeDescriptions[1].binding = 0;
        attributeDescriptions[1].location = 1;
        attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;
        attributeDescriptions[1].offset = offsetof(Vertex, color);

        attributeDescriptions[2].binding = 0;
        attributeDescriptions[2].location = 2;
        attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT;
        attributeDescriptions[2].offset = offsetof(Vertex, texCoord);

        return attributeDescriptions;
    }
};

修改Vertex结构以包含纹理坐标的vec2。确保还添加VkVertexInputAttributeDescription,以便我们可以将访问纹理坐标用作顶点着色器中的输入。这对于能够将它们传递到片段着色器以在正方形表面上进行插值是必要的。

const std::vector<Vertex> vertices = {
    {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}},
    {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}},
    {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
    {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}}
};

在本教程中,我将使用从左上角的0,0到右下角的1,1的坐标,简单地用纹理填充正方形。请随意尝试不同的坐标。尝试使用低于0或高于1的坐标来查看正在运行的寻址模式!

Shaders

最后一步是修改着色器以从纹理中采样颜色。我们首先需要修改顶点着色器,以将纹理坐标传递给片段着色器:

layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec2 inTexCoord;

layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec2 fragTexCoord;

void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
    fragColor = inColor;
    fragTexCoord = inTexCoord;
}

与逐顶点颜色一样,fragTexCoord值将通过光栅化器在正方形区域内平滑插值。我们可以通过让片段着色器将纹理坐标输出为颜色来可视化:

#version 450

layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec2 fragTexCoord;

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(fragTexCoord, 0.0, 1.0);
}

您应该看到类似下图的内容。不要忘记重新编译着色器!

绿色通道表示水平坐标,红色通道表示垂直坐标。黑色和黄色角确认纹理坐标在正方形上从0,0到1,1正确插值。使用颜色可视化数据相当于printf调试的着色器编程,因为缺少更好的选项!

组合图像采样器描述符在GLSL中由采样器制服表示。在片段着色器中添加对它的引用:

layout(binding = 1) uniform sampler2D texSampler;

对于其他类型的图像,有等效的sampler1D和sampler3D类型。确保此处使用正确的绑定。

void main() {
    outColor = texture(texSampler, fragTexCoord);
}

使用内置纹理函数对纹理进行采样。它以采样器和坐标作为参数。采样器自动处理背景中的过滤和转换。现在,当您运行应用程序时,应该可以看到正方形上的纹理:

尝试通过将纹理坐标缩放到大于1的值来尝试寻址模式。例如,当使用VK_SAMPLER_ADDRESS_MODE_REPAT时,以下片段着色器在下图中生成结果:

void main() {
    outColor = texture(texSampler, fragTexCoord * 2.0);
}

还可以使用顶点颜色操纵纹理颜色:

void main() {
    outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0);
}

我在这里分离了RGB和alpha通道,以不缩放alpha通道。

现在您知道如何访问着色器中的图像了!这是一种非常强大的技术,当与帧缓冲区中也写入的图像相结合时。您可以使用这些图像作为输入,在3D世界中实现酷炫的效果,如后期处理和相机显示。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值