#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
到目前为止,几何体是使用逐顶点颜色着色的,这是一种相当有限的方法。 在本教程的这一部分中,我们将实现纹理映射,使几何体看起来更有趣。 这也将允许我们在未来的章节中加载和绘制基本的 3D 模型。
为我们的应用程序添加纹理将涉及以下步骤:
创建一个由设备内存支持的图像对象
从图像文件中填充像素数据
创建一个图像采样器
添加一个组合图像采样器描述符,用于从纹理中采样颜色
我们之前已经使用过图像对象,但那些是由交换链扩展自动创建的。 这次我们将必须自己创建一个。 创建图像并用数据填充它类似于顶点缓冲的创建,但由于 GPU 处理图像的方式,会增加一些复杂性。 我们将首先创建一个暂存资源并填充像素数据,然后将其复制到我们将用于渲染的最终图像对象中。 虽然可以为此目的创建一个暂存图像,但 Vulkan 也允许你直接将像素从缓冲复制到图像。这样更简洁、限制更少,并且通常更快。 为此,我们将首先创建一个可由主机访问的缓冲,并用像素值填充它,然后创建一个图像将像素复制到其中。 创建图像与创建缓冲没有太大区别。 它涉及查询内存需求、分配设备内存并将其绑定,就像我们之前看到的那样。
然而,在处理图像时,我们还需要额外注意一些事情。 图像可以具有不同的 布局(layouts),这些布局会影响像素在内存中的组织方式。 例如,由于图形硬件的工作方式,仅仅逐行存储像素可能不会带来最佳性能。 在对图像执行任何操作时,你必须确保它们具有在该操作中最佳使用的布局。 我们之前在指定渲染通道时实际上已经看到了一些这样的布局:
vk::ImageLayout::ePresentSrcKHR:呈现的最佳布局
vk::ImageLayout::eColorAttachmentOptimal:作为从片段着色器写入颜色的附件的最佳布局
vk::ImageLayout::eTransferSrcOptimal:作为传输操作(如 vk::raii::CommandBuffer::copyImageToBuffer)中的源的最佳布局
vk::ImageLayout::eTransferDstOptimal:作为传输操作(如 vk::raii::CommandBuffer::copyBufferToImage)中的目标的最佳布局
vk::ImageLayout::eShaderReadOnlyOptimal:从着色器采样的最佳布局
转换图像布局的最常见方法之一是使用 管线屏障(pipeline barrier)。
管线屏障主要用于同步对资源的访问,例如确保在读取图像之前已完成写入,但它们也可用于转换布局。
在本章中,我们将了解管线屏障如何用于此目的。
屏障还可以用于在使用 vk::SharingMode::eExclusive 时传输队列族所有权。
像这样包含图像库:
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
默认情况下,该头文件只定义函数的原型。
一个代码文件需要在包含头文件时定义 STB_IMAGE_IMPLEMENTATION 宏,以包含函数体,否则我们会遇到链接错误。
void initVulkan()
{
...
createCommandPool();
createTextureImage();
createVertexBuffer();
...
}
...
void createTextureImage()
{
}
创建一个新函数 createTextureImage,我们将在此加载图像并将其上传到 Vulkan 图像对象。
我们将使用命令缓冲,因此应该在 createCommandPool 之后调用它。
在 shaders 目录旁边创建一个新目录 textures,用于存储纹理图像。
我们将从该目录加载一个名为 texture.jpg 的图像。
我选择了以下 CC0 许可图像,并将其调整为 512 x 512 像素,但你可以随意选择任何你喜欢的图像。
该库支持大多数常见的图像文件格式,如 JPEG、PNG、BMP 和 GIF。
使用此库加载图像非常简单:
void createTextureImage()
{
int texWidth, texHeight, texChannels;
stbi_uc *pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
vk::DeviceSize imageSize = texWidth * texHeight * 4;
if (!pixels)
{
throw std::runtime_error("failed to load texture image!");
}
}
stbi_load 函数接受文件路径和要加载的通道数作为参数。
STBI_rgb_alpha 值强制加载图像时包含 alpha 通道,即使它没有,这对于将来与其他纹理保持一致很有好处。
中间三个参数是用于输出图像的宽度、高度和实际通道数的变量。
返回的指针是像素值数组的第一个元素。
在 STBI_rgb_alpha 的情况下,像素逐行排列,每个像素 4 字节,总共有 texWidth * texHeight * 4 个值。
接下来,我们需要将图像上传到 GPU,以便在着色器读取期间实现最佳访问。为此,我们将创建一个位于主机可见内存中的缓冲,我们可以映射它以复制像素。此缓冲将是复制该数据到 GPU 的源。
注意:图像应始终驻留在 GPU 内存中。将它们留在仅主机可见的内存中会要求 GPU 每帧都通过 PCI 接口读取数据,其带宽远小于 GPU 内存。这将导致严重的性能影响。暂存是将图像数据放入 GPU 内存的过程。这并非总是必需的,因为设备可能提供既主机可见又是设备本地的内存类型。在这种配置下可以跳过暂存。
缓冲应位于主机可见内存(eHostVisible)中,以便我们可以映射它。它还应该是主机一致的(eHostCoherent),以确保写入其中的数据立即可用(并且不会以某种方式缓存)。并且它应该可用作传输源(eTransferSrc),以便我们稍后可以将其复制到图像:
自动类型化的返回值分别是一个 vk::raii::Buffer 和一个 vk::raii::DeviceMemory。
auto [stagingBuffer, stagingBufferMemory] =
createBuffer(imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
createBuffer(imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffer, stagingBufferMemory);
然后,我们可以直接将从图像加载库获得的像素值复制到缓冲:
void* data = stagingBufferMemory.mapMemory(0, imageSize);
memcpy(data, pixels, imageSize);
stagingBufferMemory.unmapMemory();
记得现在清理原始的像素数组:
stbi_image_free(pixels);
尽管我们可以设置着色器来访问缓冲中的像素值,但最好在 Vulkan 中使用图像对象来实现此目的。 图像对象将使我们更容易、更快地检索颜色,例如通过允许我们使用 2D 坐标。 图像对象中的像素被称为纹素(texels),从现在起我们将使用这个名称。 添加以下新的类成员:
vk::raii::Image textureImage = nullptr;
vk::raii::DeviceMemory textureImageMemory = nullptr;
图像的参数在 vk::ImageCreateInfo 结构体中指定:
vk::ImageCreateInfo imageInfo{.imageType = vk::ImageType::e2D,
.format = format,
.extent = {width, height, 1},
.mipLevels = 1,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.tiling = tiling,
.usage = usage,
.sharingMode = vk::SharingMode::eExclusive};
图像类型在 imageType 字段中指定,告诉 Vulkan 将使用何种坐标系来寻址图像中的纹素。
可以创建 1D、2D 和 3D 图像。
一维图像可用于存储数据数组或渐变,二维图像主要用于纹理,三维图像可用于存储体素体积,例如。
extent 字段指定图像的尺寸,基本上是每个轴上有多少个纹素。
这就是为什么 depth 必须为 1 而不是 0。
我们的纹理将不是数组,并且我们现在不会使用 mipmap。
Vulkan 支持许多可能的图像格式,但我们应该对纹素使用与缓冲中像素相同的格式,否则复制操作将失败。
tiling 指定纹素在内存中的排列方式。Vulkan 支持两种根本不同的模式:
vk::ImageTiling::eOptimal:纹素以实现相关的方式排列。这会导致更高效的内存访问。
vk::ImageTiling::eLinear:纹素按行主序排列在内存中,每行可能有一些填充。
注意:线性平铺图像受到很大限制。例如,它们可能只适用于 2D 图像,不能用于深度/模板,不能有多个 mip 级别或层。GPU 访问也比最优平铺图像慢得多。因此它们的用例非常少。
因此,我们将使用 vk::ImageTiling::eOptimal 以实现着色器的高效访问。
图像的 initialLayout 只有两个可能的值:
vk::ImageLayout::eUndefined:GPU 不可用,第一次转换将丢弃纹素。
vk::ImageLayout::ePreinitialized:GPU 不可用,但第一次转换将保留纹素。
在第一次转换期间需要保留纹素的情况很少。 然而,一个例子是,如果你想使用线性平铺图像作为暂存图像。 在这种情况下,你希望将纹素数据上传到它,然后转换图像为传输源而不丢失数据。
然而,在我们的例子中,我们首先将图像转换为传输目标,然后从缓冲对象复制纹素数据到它,因此我们不需要此属性,可以安全地使用 vk::ImageLayout::eUndefined。
usage 字段与缓冲创建时的语义相同。
该图像将用作缓冲复制的目标,因此它应设置为传输目标。
我们还希望能够在着色器中访问图像以为网格着色,因此用途应包含 vk::ImageUsageFlagBits::eSampled。
该图像将仅由一个队列族使用:支持图形(因此也支持)传输操作的队列族。
samples 标志与多重采样有关。
这仅对将用作附件的图像相关,因此坚持使用一个采样。
image = vk::raii::Image(device, imageInfo);
使用 vk::raii::Image 构造函数创建图像,它没有任何特别值得注意的参数。
有可能图形硬件不支持 vk::Format::eR8G8B8A8Srgb 格式。
你应该有一个可接受的替代格式列表,并选择支持的最佳格式。
然而,对这种特定格式的支持非常普遍,以至于我们将跳过这一步。
使用不同的格式也需要烦人的转换。
我们将在深度缓冲章节中回到这一点,届时我们将实现这样的系统。
vk::MemoryRequirements memRequirements = image.getMemoryRequirements();
vk::MemoryAllocateInfo allocInfo{.allocationSize = memRequirements.size,
.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties)};
imageMemory = vk::raii::DeviceMemory(device, allocInfo);
image.bindMemory(imageMemory, 0);
为图像分配内存的方式与为缓冲分配内存的方式完全相同。
使用默认的 vk::raii::DeviceMemory 构造函数,并在 image 上使用 bindMemory。
这个函数已经变得相当大,而且在后续章节中还需要创建更多图像,因此我们应该将图像创建抽象为一个 createImage 函数,就像我们对缓冲所做的那样。
创建该函数,并将图像对象创建和内存分配移动到其中:
std::pair<vk::raii::Image, vk::raii::DeviceMemory> createImage(
uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties )
{
vk::ImageCreateInfo imageInfo{.imageType = vk::ImageType::e2D,
.format = format,
.extent = {width, height, 1},
.mipLevels = 1,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.tiling = tiling,
.usage = usage,
.sharingMode = vk::SharingMode::eExclusive};
vk::raii::Image image = vk::raii::Image(device, imageInfo);
vk::MemoryRequirements memRequirements = image.getMemoryRequirements();
vk::MemoryAllocateInfo allocInfo{.allocationSize = memRequirements.size,
.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties)};
vk::raii::DeviceMemory imageMemory = vk::raii::DeviceMemory(device, allocInfo);
image.bindMemory(imageMemory, 0);
return {std::move(image), std::move(imageMemory)};
}
我将宽度、高度、格式、平铺模式、用途和内存属性设为参数,因为在本教程中我们创建的图像之间这些都会有所不同。
createTextureImage 函数现在可以简化为:
void createTextureImage()
{
int texWidth, texHeight, texChannels;
stbi_uc *pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
vk::DeviceSize imageSize = texWidth * texHeight * 4;
if (!pixels)
{
throw std::runtime_error("failed to load texture image!");
}
auto [stagingBuffer, stagingBufferMemory] =
createBuffer(imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
void *data = stagingBufferMemory.mapMemory(0, imageSize);
memcpy(data, pixels, imageSize);
stagingBufferMemory.unmapMemory();
stbi_image_free(pixels);
std::tie(textureImage, textureImageMemory) = createImage(texWidth,
texHeight,
vk::Format::eR8G8B8A8Srgb,
vk::ImageTiling::eOptimal,
vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled,
vk::MemoryPropertyFlagBits::eDeviceLocal);
}
如前所述,Vulkan 中的图像可以存在于不同的布局中,这些布局影响像素数据在内存中的组织方式。这些布局针对特定操作进行了优化——有些布局更适合从着色器读取,有些更适合作为渲染目标,还有一些更适合作为传输操作的源或目标。
布局转换是 Vulkan 设计的一个关键方面,它让你对这些内存组织拥有显式控制。与其他一些图形 API 中驱动程序自动处理这些转换不同,Vulkan 要求你显式管理它们。这种方法允许更好的性能优化,因为你可以精确地在需要时调度转换,并高效地批处理操作。
对于我们的纹理图像,我们需要执行几个转换: 1. 从初始未定义布局转换为适合接收数据的布局(传输目标) 2. 从传输目标转换为适合着色器读取的布局,以便我们的片段着色器可以从中采样
这些转换使用管线屏障执行,这不仅改变图像布局,还确保访问图像的操作之间的正确同步。如果没有正确的同步,可能会出现竞争条件,即着色器在复制操作完成之前尝试读取纹理。
我们现在要编写的函数涉及再次记录和执行命令缓冲,因此现在是时候将该逻辑移到一个或两个辅助函数中:
vk::raii::CommandBuffer beginSingleTimeCommands()
{
vk::CommandBufferAllocateInfo allocInfo{.commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1};
vk::raii::CommandBuffer commandBuffer = std::move(vk::raii::CommandBuffers(device, allocInfo).front());
vk::CommandBufferBeginInfo beginInfo{.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit};
commandBuffer.begin(beginInfo);
return std::move(commandBuffer);
}
void endSingleTimeCommands(vk::raii::CommandBuffer &&commandBuffer)
{
commandBuffer.end();
vk::SubmitInfo submitInfo{.commandBufferCount = 1, .pCommandBuffers = &*commandBuffer};
queue.submit(submitInfo, nullptr);
queue.waitIdle();
}
这些函数的代码基于 copyBuffer 中的现有代码。
你现在可以将该函数简化为:
void copyBuffer(vk::raii::Buffer &srcBuffer, vk::raii::Buffer &dstBuffer, vk::DeviceSize size)
{
vk::raii::CommandBuffer commandCopyBuffer = beginSingleTimeCommands();
commandCopyBuffer.copyBuffer(*srcBuffer, *dstBuffer, vk::BufferCopy{.size = size});
endSingleTimeCommands(std::move(commandCopyBuffer));
}
如果我们仍然使用缓冲,那么我们现在可以编写一个函数来记录并执行 copyBufferToImage 以完成工作,但此命令要求图像首先处于正确的布局。
创建一个新函数来处理布局转换:
void transitionImageLayout(vk::raii::CommandBuffer &commandBuffer, const vk::raii::Image &image, vk::ImageLayout oldLayout, vk::ImageLayout newLayout)
{
}
执行布局转换的最常见方法之一是使用 图像内存屏障(image memory barrier)。
这样的管线屏障通常用于同步对资源的访问,例如确保在读取缓冲之前完成对缓冲的写入,但它也可用于转换图像布局,并在使用 vk::SharingMode::eExclusive 时传输队列族所有权。
有一个等效的 缓冲内存屏障(buffer memory barrier) 可以对缓冲执行此操作。
vk::ImageMemoryBarrier barrier{.oldLayout = oldLayout,
.newLayout = newLayout,
.srcQueueFamilyIndex = vk::QueueFamilyIgnored,
.dstQueueFamilyIndex = vk::QueueFamilyIgnored,
.image = image,
.subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .levelCount = 1, .layerCount = 1}};
oldLayout 和 newLayout 指定布局转换。
如果你不关心图像的现有内容,可以使用 vk::ImageLayout::eUndefined 作为 oldLayout。
如果你使用屏障传输队列族所有权,则 srcQueueFamilyIndex 和 dstQueueFamilyIndex 字段应为队列族的索引。
如果你不想这样做,则必须将它们设置为 vk::QueueFamilyIgnored(不是默认值!)。
image 和 subresourceRange 指定受影响的图像以及图像的特定部分。
我们的图像不是数组,也没有 mipmap 级别,因此只指定了一个级别和层。
屏障主要用于同步目的,因此你必须指定哪些涉及该资源的操作必须在屏障之前发生,以及哪些涉及该资源的操作必须等待屏障。 正确的值取决于旧布局和新布局,因此一旦我们确定了要使用的转换,我们将回到这一点。
commandBuffer.pipelineBarrier(sourceStage, destinationStage, {}, {}, {}, barrier);
所有类型的管线屏障都使用相同的函数提交。
第一个参数指定在屏障之前应该发生的操作所在的管线阶段。
第二个参数指定操作将等待屏障的管线阶段。
你允许在屏障之前和之后指定的管线阶段取决于你在屏障之前和之后如何使用资源。
允许的值列在规范的此表中。
例如,如果你将在屏障之后从 uniform 读取,则指定 vk::AccessFlagBits::eUniformRead 的用途,并将最早读取 uniform 的着色器作为管线阶段,例如 vk::PipelineStageFlagBits::eFragmentShader。
为此类用途指定非着色器管线阶段是没有意义的,当你指定与用途类型不匹配的管线阶段时,验证层会发出警告。
第三个参数是 {} 或 vk::DependencyFlagBits::eByRegion。
后者将屏障转换为逐区域条件。
这意味着允许实现已经开始读取资源中到目前为止已写入的部分,例如。
最后三个参数引用三种可用类型的管线屏障数组:内存屏障、缓冲内存屏障和图像内存屏障,就像我们在这里使用的那个。
在回到 createTextureImage 之前,我们将再编写一个辅助函数:copyBufferToImage:
void copyBufferToImage(vk::raii::CommandBuffer &commandBuffer, const vk::raii::Buffer &buffer, vk::raii::Image &image, uint32_t width, uint32_t height)
{
}
与缓冲复制一样,你需要指定缓冲的哪个部分将复制到图像的哪个部分。
这通过 vk::BufferImageCopy 结构体实现:
vk::BufferImageCopy region{.bufferOffset = 0,
.bufferRowLength = 0,
.bufferImageHeight = 0,
.imageSubresource = {.aspectMask = vk::ImageAspectFlagBits::eColor, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = 1},
.imageOffset = {0, 0, 0},
.imageExtent = {width, height, 1}};
这些字段大多不言自明。
bufferOffset 指定缓冲中像素值开始的字节偏移量。
bufferRowLength 和 bufferImageHeight 字段指定像素在内存中的布局方式。
例如,你可以在图像的行之间有一些填充字节。
将两者都指定为 0 表示像素是紧密打包的,就像我们的情况一样。
imageSubresource、imageOffset 和 imageExtent 字段指示我们想要将像素复制到图像的哪个部分。
缓冲到图像的复制操作使用 vk::raii::CommandBuffer::copyBufferToImage 函数入队:
commandBuffer.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, region);
第三个参数指示图像当前使用的布局。
我在这里假设图像已经转换为最适合复制像素的布局。
现在我们只将一块像素复制到整个图像,但可以指定一个 vk::BufferImageCopy 数组,在一次操作中执行从该缓冲到图像的许多不同复制。
我们现在拥有了完成设置纹理图像所需的所有工具,因此我们回到 createTextureImage 函数。
我们最后在那里做的是创建纹理图像。
下一步是将暂存缓冲复制到纹理图像。
这涉及两个步骤:
将纹理图像转换为 vk::ImageLayout::eTransferDstOptimal
执行缓冲到图像的复制操作
使用我们刚刚创建的函数很容易做到:
vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands();
transitionImageLayout(commandBuffer, textureImage, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal);
copyBufferToImage(commandBuffer, stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));
图像是使用 vk::ImageLayout::eUndefined 布局创建的,因此在转换 textureImage 时,应将其指定为旧布局。
请记住,我们可以这样做,因为我们在执行复制操作之前不关心其内容。
为了能够在着色器中开始从纹理图像采样,我们需要最后一次转换,以准备它供着色器访问。 最后,单次命令缓冲可以结束。
transitionImageLayout(commandBuffer, textureImage, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);
endSingleTimeCommands(std::move(commandBuffer));
如果你现在在启用验证层的情况下运行应用程序,你会发现它抱怨 transitionImageLayout 中的访问掩码和管线阶段无效。
我们仍然需要根据转换中的布局来设置这些。
我们需要处理两个转换:
未定义 → 传输目标:传输写入不需要等待任何东西
传输目标 → 着色器读取:着色器读取应等待传输写入,特别是片段着色器中的着色器读取,因为那是我们将使用纹理的地方
这些规则使用以下访问掩码和管线阶段指定:
vk::PipelineStageFlags sourceStage;
vk::PipelineStageFlags destinationStage;
if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal)
{
barrier.srcAccessMask = {};
barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
sourceStage = vk::PipelineStageFlagBits::eTopOfPipe;
destinationStage = vk::PipelineStageFlagBits::eTransfer;
}
else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal)
{
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
sourceStage = vk::PipelineStageFlagBits::eTransfer;
destinationStage = vk::PipelineStageFlagBits::eFragmentShader;
}
else
{
throw std::invalid_argument("unsupported layout transition!");
}
commandBuffer.pipelineBarrier(sourceStage, destinationStage, {}, {}, nullptr, barrier);
如前述表格所示,传输写入必须在管线传输阶段发生。
由于写入不需要等待任何东西,你可以为屏障前的操作指定空访问掩码和最早的管线阶段 vk::PipelineStageFlagBits::eTopOfPipe。
应该注意的是,vk::PipelineStageFlagBits::eTransfer 不是图形和计算管线中的 真实 阶段。
它更像是发生传输的伪阶段。
有关更多信息和其他伪阶段示例,请参阅文档。
图像将在同一管线阶段被写入,随后由片段着色器读取,这就是为什么我们在片段着色器管线阶段指定着色器读取访问。
如果将来我们需要进行更多转换,我们将扩展该函数。 应用程序现在应该成功运行,尽管当然还没有视觉变化。
需要注意的一件事是,命令缓冲提交会在开始时产生隐式的 vk::AccessFlagBits::eHostWrite 同步。
由于 transitionImageLayout 函数执行一个只包含单个命令的命令缓冲,如果你在布局转换中需要 vk::AccessFlagBits::eHostWrite 依赖关系,你可以利用这种隐式同步并将 srcAccessMask 设置为 0。
你是否要显式指定取决于你,但我个人不喜欢依赖这些类似 OpenGL 的“隐藏”操作。
实际上有一种特殊的图像布局支持所有操作,即 vk::ImageLayout::eGeneral。
但除非使用某些扩展(我们在本教程中不使用),否则使用通用布局可能会带来性能损失,因为它可能会在某些 GPU 上禁用某些优化。
在某些特殊情况下,例如将图像同时用作输入和输出,或者在图像离开预初始化布局后读取它,需要使用它。
现在图像包含了纹理,但我们仍然需要一种从图形管线访问它的方法。 我们将在下一章中处理这个问题。
C++ 代码 / slang 着色器 / GLSL 顶点着色器 / GLSL 片段着色器
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。