操作系统:Windows8.1

显卡:Nivida GTX965M

开发工具:Visual Studio 2017


Introduction

到目前为止,几何图形使用每个顶点颜色进行着色处理,这是一个局限性比较大的方式。在本教程的一部分内容中,我们实现纹理映射,使得几何图形看起来更加生动有趣。这部分使我们在未来的章节中加载和绘制基本的3D模型。

 

添加一个贴图到应用程序需要以下几个步骤:

  • 创建设备内存支持的图像对象

  • 从图像文件填充像素

  • 创建图像采样器

  • 添加组合的图像采样器描述符,并从纹理采样颜色信息

我们之前已经使用过图像对象,但是它们都是由交换链扩展自动创建的。这次我们将要自己创建。创建一个图像及填充数据与之前的顶点缓冲区创建类似。我们开始使用暂存资源并使用像素数据进行填充,接着将其拷贝到最终用于渲染使用的图像对象中。尽管可以为此创建一个暂存图像,Vulkan也允许从VkBuffer中拷贝像素到图像中,这部分API在一些硬件上非常有效率 faster on some hardware。我们首先会创建缓冲区并通过像素进行填充,接着创建一个图像对象拷贝像素。创建一个图像与创建缓冲区类似。就像我们之前看到的那样,它需要查询内存需求,分配设备内存并进行绑定。

 

然而,仍然有一些额外的工作需要面对,当我们使用图像的时候。我们知道图像可以有不同的布局,它影响实际像素在内存中的组织。由于图形硬件的工作原理,简单的逐行存储像素可能不是最佳的性能选择。对图像执行任何操作时,必须确保它们有最佳的布局,以便在该操作中使用。实际上我们已经在指定渲染通道的时候看过这些布局类型:

  • VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: 用于呈现,使用最佳

  • VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: 当使用附件从片段着色器进行写入时候,使用最佳

  • VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: 作为传送源操作的时候,使用最佳,比如vkCmdCopyImageToBuffer

  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: 最为传输目的地的时候,使用最佳,比如vkCmdCopyBufferToImage

  • VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: 着色器中用于采样,使用最佳

变换图像布局的最常见方式之一是管线屏障 pipeline barrier。管线屏障主要用于同步访问资源,诸如确保图像在读之前写入,但是也可以用于布局变换。在本章节中我们将会看到如何使用管线屏障完成此任务。除此之外,屏障也可以用于VK_SHARING_MODE_EXCLUSIVE模式下队列簇宿主的变换。

Image library


用于加载图片的库有很多,甚至可以自己编写代码加载简单格式的图片比如BMP和PPM。在本教程中我们将会使用stb_image库。优势是所有的代码都在单一的文件中,所以它不需要任何棘手的构建配置。下载stb_image.h头文件并将它保存在方便的位置,在这里我们存放与GLFW、GLM、vulkan头文件的相同的目录中 3rdparty\Include 下,如图所示:

iOS培训,Swift培训,苹果开发培训,移动开发培训

Visual Studio

确认$(SolutionDir)\3rdparty\Include添加到 Additional Include Directories 路径中。

iOS培训,Swift培训,苹果开发培训,移动开发培训

Loading an image


包含image库的头文件:

#define STB_IMAGE_IMPLEMENTATION#include <stb_image.h>

默认情况下头文件仅仅定义了函数的原型。一个代码文件需要使用STB_IMAGE_IMPLEMENTATION定义包含头文件中定义的函数体,否则会收到链接错误。

iOS培训,Swift培训,苹果开发培训,移动开发培训

void initVulkan() {
    ...
    createCommandPool();
    createTextureImage();
    createVertexBuffer();
    ...
}

...void createTextureImage() {

}

iOS培训,Swift培训,苹果开发培训,移动开发培训

创建新的函数createTextureImage用于加载图片和提交到Vulkan图像对象中。我们也会使用命令缓冲区,所以需要在createCommandPool之后调用。

 

shaders目录下新增新的textures目录,用于存放贴图资源。我们将会从目录中加载名为texture.jpg的图像。这里选择 CC0 licensed image 并调整为512 x 512像素大小,但是在这里可以使用任何你期望的图片。库支持很多主流的图片文件格式,比如JPEG,PNG,BMP和GIF。

iOS培训,Swift培训,苹果开发培训,移动开发培训

使用库加载图片是非常容易的:

iOS培训,Swift培训,苹果开发培训,移动开发培训

void createTextureImage() {    int texWidth, texHeight, texChannels;
    stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
    VkDeviceSize imageSize = texWidth * texHeight * 4;    if (!pixels) {        throw std::runtime_error("failed to load texture image!");
    }
}

iOS培训,Swift培训,苹果开发培训,移动开发培训

stbi_load函数使用文件的路径和通道的数量作为参数加载图片。STBI_rgb_alpha值强制加载图片的alpha通道,及时它本身没有alpha,但是这样做对于将来加载其他的纹理的一致性非常友好。中间三个参数用于输出width, height 和实际的图片通道数量。返回的指针是像素数组的第一个元素地址。总共 texWidth * texHeight * 4 个像素值,像素在STBI_rgba_alpha的情况下逐行排列,每个像素4个字节。

Staging buffer


我们现在要在host visible内存中创建一个缓冲区,以便我们可以使用vkMapMemory并将像素复制给它。在createTextureImage函数中添临时缓冲区变量。

VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;

缓冲区必须对于host visible内存可见,为此我们对它进行映射,之后使用它作为传输源拷贝像素到头像对象中。

createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

我们可以直接从库中加载的图片中拷贝像素到缓冲区:

void* data;
vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data);
    memcpy(data, pixels, static_cast<size_t>(imageSize));
vkUnmapMemory(device, stagingBufferMemory);

不要忘记清理原图像的像素数据:

stbi_image_free(pixels);

Texture Image


虽然我们可以通过设置着色器访问缓冲区中的像素值,但是在Vulkan中最好使用image对象完成该操作。图像对象可以允许我们使用而二维坐标来更容易的快速的检索颜色。图像中的像素被成为纹素即纹理元素,我们将从此处开始使用该名称。添加以下新的类成员:

VkImage textureImage;
VkDeviceMemory textureImageMemory;

对于图像的参数通过VkImageCreateInfo结构体来描述:

iOS培训,Swift培训,苹果开发培训,移动开发培训

VkImageCreateInfo imageInfo = {};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = static_cast<uint32_t>(texWidth);
imageInfo.extent.height = static_cast<uint32_t>(texHeight);
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;

iOS培训,Swift培训,苹果开发培训,移动开发培训

imageType字段指定图像类型,告知Vulkan采用什么样的坐标系在图像中采集纹素。它可以是1D,2D和3D图像。1D图像用于存储数组数据或者灰度图,2D图像主要用于纹理贴图,3D图像用于存储立体纹素。extent字段指定图像的尺寸,基本上每个轴上有多少纹素。这就是为什么深度必须是1而不是0。我们的纹理不会是一个数组,而现在我们不会使用mipmapping功能。

imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;

Vulkan支持多种图像格式,但无论如何我们要在缓冲区中为纹素应用与像素一致的格式,否则拷贝操作会失败。

imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;

tiling字段可以设定两者之一:

  • VK_IMAGE_TILING_LINEAR: 纹素基于行主序的布局,如pixels数组

  • VK_IMAGE_TILING_OPTIMAL: 纹素基于具体的实现来定义布局,以实现最佳访问

与不像布局不同的是,tiling模式不能在之后修改。如果需要在内存图像中直接访问纹素,必须使用VK_IMAGE_TILING_LINEAR。我们将会使用暂存缓冲区代替暂存图像,所以这部分不是很有必要。为了更有效的从shader中访问纹素,我们将会使用VK_IMAGE_TILING_OPTIMAL

imageInfo.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;

对于图像的initialLayout字段,仅有两个可选的值:

  • VK_IMAGE_LAYOUT_UNDEFINED: GPU不能使用,第一个变换将丢弃纹素。

  • VK_IMAGE_LAYOUT_PREINITIALIZED: GPU不能使用,但是第一次变换将会保存纹素。

最初未定义的布局适用于将用作附件的图像,如颜色和深度缓冲区。在这个情况下我们不关心任何初始的数据,因为它很可能会在使用前被render pass清理掉。如果你想填充数据,比如贴图,你应该使用preinitialized layout。

imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;

usage字段在缓冲区创建过程中有相同的语意。图像将会被用作缓冲区拷贝的目标,所以应该设置作为传输目的地。我们还希望从着色器中访问图像对我们的mesh进行着色,因此具体的usage还要包括VK_IMAGE_USAGE_SAMPLED_BIT

imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

 因为图像会在一个队列簇中使用:支持图形或者传输操作。

imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.flags = 0; // Optional

 samples标志位与多重采样相关。这仅仅适用于作为附件的图像,所以我们坚持一个采样数值。与稀疏图像相关的图像有一些可选的标志。稀疏图像是仅仅某些区域实际上被存储器支持的图像。例如,如果使用3D纹理进行立体地形,则可以使用此方法来避免分配内存来存储大量“空气”值。我们不会在本教程中使用,所以设置默认值0

if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) {    throw std::runtime_error("failed to create image!");
}

使用vkCreateImage创建图像,这里没有任何特殊的参数设置。可能图形硬件不支持VK_FORMAT_R8G8B8A8_UNORM格式。我们应该持有一个可以替代的可以接受的列表。然而对这种特定格式的支持是非常普遍的,我们将会跳过这一步。使用不同的格式也需要繁琐的转换过程。我们会回到深度缓冲区章节,实现类似的系统。

iOS培训,Swift培训,苹果开发培训,移动开发培训

VkMemoryRequirements memRequirements;
vkGetImageMemoryRequirements(device, textureImage, &memRequirements);

VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);if (vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory) != VK_SUCCESS) {    throw std::runtime_error("failed to allocate image memory!");
}

vkBindImageMemory(device, textureImage, textureImageMemory, 0);

iOS培训,Swift培训,苹果开发培训,移动开发培训

为图像工作分配内存与为缓冲区分配内存是类似的,使用vkGetImageMemoryRequirements代替vkGetBufferMemoryRequirements,并使用vkBindImageMemory代替vkBindBufferMemory

 

这个函数已经变得比较庞大臃肿了,而且需要在后面的章节中创建更多的图像,所以我们应该将图像创建抽象成一个createImage函数,就像之前为buffers缓冲区做的事情一样。创建函数并将图像对象的创建和内存分配移动过来:

iOS培训,Swift培训,苹果开发培训,移动开发培训

void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) {
    VkImageCreateInfo imageInfo = {};
    imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
    imageInfo.imageType = VK_IMAGE_TYPE_2D;
    imageInfo.extent.width = width;
    imageInfo.extent.height = height;
    imageInfo.extent.depth = 1;
    imageInfo.mipLevels = 1;
    imageInfo.arrayLayers = 1;
    imageInfo.format = format;
    imageInfo.tiling = tiling;
    imageInfo.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;
    imageInfo.usage = usage;
    imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
    imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;    if (vkCreateImage(device, &imageInfo, nullptr, &image) != VK_SUCCESS) {        throw std::runtime_error("failed to create image!");
    }

    VkMemoryRequirements memRequirements;
    vkGetImageMemoryRequirements(device, image, &memRequirements);

    VkMemoryAllocateInfo allocInfo = {};
    allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    allocInfo.allocationSize = memRequirements.size;
    allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);    if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) {        throw std::runtime_error("failed to allocate image memory!");
    }

    vkBindImageMemory(device, image, imageMemory, 0);
}

iOS培训,Swift培训,苹果开发培训,移动开发培训

这里使用了width, height, format, tiling mode, usage和memory properties参数,因为这些参数根据教程中创建的图像而不同。

 

createTextureImage函数现在简化为:

iOS培训,Swift培训,苹果开发培训,移动开发培训

void createTextureImage() {    int texWidth, texHeight, texChannels;
    stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
    VkDeviceSize imageSize = texWidth * texHeight * 4;    if (!pixels) {        throw std::runtime_error("failed to load texture image!");
    }

    VkBuffer stagingBuffer;
    VkDeviceMemory stagingBufferMemory;
    createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);    void* data;
    vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data);
        memcpy(data, pixels, static_cast<size_t>(imageSize));
    vkUnmapMemory(device, stagingBufferMemory);

    stbi_image_free(pixels);

    createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);
}

iOS培训,Swift培训,苹果开发培训,移动开发培训

Layout transitions


我们将要编写的函数会涉及到记录和执行命令缓冲区,所以现在适当的移除一些逻辑到辅助函数中去:

iOS培训,Swift培训,苹果开发培训,移动开发培训

VkCommandBuffer beginSingleTimeCommands() {
    VkCommandBufferAllocateInfo allocInfo = {};
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    allocInfo.commandPool = commandPool;
    allocInfo.commandBufferCount = 1;

    VkCommandBuffer commandBuffer;
    vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);

    VkCommandBufferBeginInfo beginInfo = {};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

    vkBeginCommandBuffer(commandBuffer, &beginInfo);    return commandBuffer;
}void endSingleTimeCommands(VkCommandBuffer commandBuffer) {
    vkEndCommandBuffer(commandBuffer);

    http://www.cnblogs.com/heitao/p/7102419.html