Vulkan 纹理与帧缓冲:从加载到渲染的完整流程

全面拆解 Vulkan 纹理与帧缓冲体系的核心机制,涵盖 Image 对象属性、ImageView/Sampler 配置、Framebuffer 附件绑定、MSAA 解析与 VK_KHR_dynamic_rendering 动态渲染扩展,配合完整 C++ 代码示例。

纹理与帧缓冲是图形渲染管线中最基础也最关键的两个数据载体。理解Vulkan在这两个领域的实现细节,对于构建高性能渲染引擎至关重要。

一、Image 对象的完整创建流程

Vulkan中的Image对象承载了GPU显存中的图像数据。与Buffer不同,Image支持多维度的内存布局、多种采样格式以及丰富的使用场景。创建Image时需要指定一系列关键参数,这些参数共同决定了Image在GPU内部的存储方式和可用性。

1.1 核心属性解析

创建VkImage时,以下参数定义了其基本形态:

  • imageType:图像维度,可以是1D、2D或3D。2D图像用于常规纹理和帧缓冲附件,1D用于特定效果如渐变图,3D用于体积雾、3D噪声等。
  • format:像素格式,直接决定了每个像素的内存占用和通道布局。常用格式包括VK_FORMAT_R8G8B8A8_UNORM(8位RGBA无归一化)、VK_FORMAT_B8G8R8A8_UNORM(BGRA交换)、VK_FORMAT_D32_SFLOAT(32位单精度深度)等。
  • extent:三维度量,对于2D纹理height和depth分别为图像高度和1。
  • mipLevels:mipmap层级数。层级1表示无mipmap,大于1时每个层级尺寸减半,用于不同距离下的纹理采样优化。
  • arrayLayers:数组层数。大于1时可用于纹理数组或CubeMap的6个面。
  • tiling:内存布局模式。VK_IMAGE_TILING_LINEAR的内存布局与CPU可线性遍历,性能较差;VK_IMAGE_TILING_OPTIMAL由驱动按GPU最友好的方式排布,是纹理和渲染附件的首选。
  • usage:使用场景标志位,可以是VK_IMAGE_USAGE_SAMPLED_BIT(着色器采样)、VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT(颜色附件)、VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT(深度模板附件)、VK_IMAGE_USAGE_TRANSFER_SRC_BIT/DST_BIT(传输源/目标)等的组合。

1.2 具体创建代码

以下是创建一个典型的2D可采样颜色纹理Image的配置:

VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = 2048;
imageInfo.extent.height = 2048;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 11;  // log2(2048) + 1
imageInfo.arrayLayers = 1;
imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT
                | VK_IMAGE_USAGE_SAMPLED_BIT
                | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; // 用于mipmap生成
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;

VkImage textureImage;
vkCreateImage(device, &imageInfo, nullptr, &textureImage);

这里的关键细节在于initialLayout必须是VK_IMAGE_LAYOUT_UNDEFINEDVK_IMAGE_LAYOUT_PREINITIALIZED。对于GPU最优tiling的Image,前者的语义是"我不关心之前的内存内容",后者仅用于CPU可访问的线性tiling Image以保留初始数据。从Staging Buffer上传纹理数据后,必须显式地通过Image Memory Barrier将其布局从VK_IMAGE_LAYOUT_UNDEFINED转换为VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,再转为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL

1.3 内存分配与绑定

Image创建后只持有逻辑句柄,还需分配设备本地显存并绑定:

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
);

VkDeviceMemory textureImageMemory;
vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory);
vkBindImageMemory(device, textureImage, textureImageMemory, 0);

生产环境中应使用VMA(Vulkan Memory Allocator)或自定义内存分配器来避免每创建一个Image就调用一次vkAllocateMemory。Vulkan驱动对内存分配数量有限制,通常几百到上千次分配就会触及上限。

二、ImageView 的多维视角

ImageView是Image的"视图",它定义了如何解释Image的内存数据。同一个Image可以创建多个View,以不同格式或访问维度的形式供管线使用。

2.1 ImageView 的核心参数

  • viewType:视图类型,与imageType对应,但更为丰富。支持VK_IMAGE_VIEW_TYPE_1DVK_IMAGE_VIEW_TYPE_2DVK_IMAGE_VIEW_TYPE_3DVK_IMAGE_VIEW_TYPE_CUBE(CubeMap)、VK_IMAGE_VIEW_TYPE_1D_ARRAYVK_IMAGE_VIEW_TYPE_2D_ARRAY等。
  • format:视图格式。通常与Image创建时一致,但也可以指定兼容格式进行重新解释。例如将VK_FORMAT_R32_UINT的Image以VK_FORMAT_R32_SFLOAT的View访问。
  • subresourceRange:定义View覆盖Image的哪些部分,包括aspectMask(颜色/深度/模板)、baseMipLevel/levelCount(mipmap层级范围)、baseArrayLayer/layerCount(数组层范围)。

2.2 各种 ImageView 的创建示例

标准2D纹理视图:

VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = textureImage;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 11;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;

VkImageView textureImageView;
vkCreateImageView(device, &viewInfo, nullptr, &textureImageView);

深度缓冲视图(仅深度通道):

viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = VK_FORMAT_D32_SFLOAT;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
vkCreateImageView(device, &viewInfo, nullptr, &depthImageView);

CubeMap视图(数组层数为6):

viewInfo.viewType = VK_IMAGE_VIEW_TYPE_CUBE;
viewInfo.format = VK_FORMAT_R16G16B16A16_SFLOAT;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.layerCount = 6; // 6个面
vkCreateImageView(device, &viewInfo, nullptr, &cubeMapView);

三、Sampler 的精细化配置

Sampler定义了着色器采样Image时的行为模式,包括过滤方式、寻址模式和mipmap策略。Sampler是独立对象,多个Image可以共享同一个Sampler配置。

3.1 过滤方式

  • VK_FILTER_NEAREST:最近邻采样,取距离采样坐标最近的纹素值。适合像素艺术、锐利的UI元素。
  • VK_FILTER_LINEAR:双线性过滤,对2x2纹素进行加权平均。适合常规纹理,在近距离下提供平滑过渡。
  • VK_SAMPLER_MIPMAP_MODE_NEAREST/LINEAR:mipmap层级间的插值模式。LINEAR模式在相邻mipmap层级间进行三线性插值,能消除mipmap切换时的跳变。

3.2 寻址模式

当纹理坐标超出[0, 1]范围时的处理方式:

  • VK_SAMPLER_ADDRESS_MODE_REPEAT:重复平铺,适合地表、墙面等无缝纹理。
  • VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE:边缘像素延伸,适合不希望出现接缝的天空盒。
  • VK_SAMPLER_ADDRESS_MODE_MIRROR_REPEAT:镜像重复,适合对称图案。
  • VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER:超出部分使用指定的边框颜色,可用于阴影贴图的PCF过滤边界处理。

3.3 各向异性过滤

各向异性过滤是修正倾斜表面纹理走样的关键技术。启用时需要检查VkPhysicalDeviceFeatures::samplerAnisotropy是否支持,并在逻辑设备创建时启用该特性:

VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = VK_FILTER_LINEAR;
samplerInfo.minFilter = VK_FILTER_LINEAR;
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.anisotropyEnable = VK_TRUE;
samplerInfo.maxAnisotropy = 16.0f; // 最高16x
samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
samplerInfo.unnormalizedCoordinates = VK_FALSE;
samplerInfo.compareEnable = VK_FALSE;
samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
samplerInfo.mipLodBias = 0.0f;
samplerInfo.minLod = 0.0f;
samplerInfo.maxLod = 11.0f; // 对应mipLevels

VkSampler textureSampler;
vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler);

上述compareEnablecompareOp用于阴影贴图的百分比接近过滤(PCF),将其与VK_COMPARE_OP_LESS配合可在Sampler阶段直接完成深度比较。

四、Framebuffer 与 Render Pass Attachments

Framebuffer将ImageView绑定到Render Pass定义的附件槽位上,是连接Render Pass描述与实际图像内存的桥梁。

4.1 Render Pass 附件描述

在创建Render Pass时,需要定义每个附件的格式、采样数和加载/存储操作:

VkAttachmentDescription colorAttachment{};
colorAttachment.format = swapChainImageFormat;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

VkAttachmentDescription depthAttachment{};
depthAttachment.format = VK_FORMAT_D32_SFLOAT;
depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; // 深度通常不需要存储
depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

loadOp决定渲染开始时如何处理附件内容:LOAD保留上一帧内容,CLEAR使用指定清除值清零,DONT_CARE允许未定义内容(可提升性能)。storeOp决定渲染结束时是否将结果写回内存。

4.2 Subpass 设计

一个Render Pass可以包含多个Subpass,现代延迟渲染管线通常至少有3个Subpass:G-Buffer填充、光照计算、后处理合成。Subpass通过输入附件引用前一Subpass的输出,相比全屏贴图采样有着更好的内存带宽利用率(On-Chip Tile Memory)。

VkAttachmentReference colorAttachmentRef{};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

VkAttachmentReference depthAttachmentRef{};
depthAttachmentRef.attachment = 1;
depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
subpass.pDepthStencilAttachment = &depthAttachmentRef;

4.3 Subpass 依赖

必须显式声明Subpass之间的执行依赖和内存依赖,以确保GPU正确地同步内存访问:

VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL; // 渲染之前的所有操作
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
                        | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
                        | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.srcAccessMask = 0;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT
                         | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;

这里VK_SUBPASS_EXTERNAL与索引0的Subpass之间建立了依赖:在Render Pass开始时,必须等待之前的颜色附件输出和早期片段测试完成,然后才能写入颜色附件和深度模板附件。

4.4 Framebuffer 的创建

Framebuffer将实际的ImageView绑定到Render Pass定义的附件槽位:

VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = 2;
framebufferInfo.pAttachments = attachments; // {colorImageView, depthImageView}
framebufferInfo.width = swapChainExtent.width;
framebufferInfo.height = swapChainExtent.height;
framebufferInfo.layers = 1;

VkFramebuffer framebuffer;
vkCreateFramebuffer(device, &framebufferInfo, nullptr, &framebuffer);

Framebuffer的维度必须与Render Pass兼容,且每个附件的layer数不能少于Framebuffer的layers。

五、Depth/Stencil Attachment 的精确控制

深度缓冲是3D渲染的核心,决定了像素级的可见性排序。

5.1 深度附件创建

VkImageCreateInfo depthInfo{};
depthInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
depthInfo.imageType = VK_IMAGE_TYPE_2D;
depthInfo.extent = {width, height, 1};
depthInfo.mipLevels = 1;
depthInfo.arrayLayers = 1;
depthInfo.format = VK_FORMAT_D32_SFLOAT; // 或 D24_UNORM_S8_UINT 含模板
depthInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
depthInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
depthInfo.samples = VK_SAMPLE_COUNT_1_BIT;
vkCreateImage(device, &depthInfo, nullptr, &depthImage);

Vulkan要求使用专门的深度/模板格式作为深度附件,VK_FORMAT_D32_SFLOAT在支持度和精度之间取得了良好平衡。对于同时需要深度和模板的场景(如几何体遮罩、模板阴影体积),使用VK_FORMAT_D24_UNORM_S8_UINTVK_FORMAT_D32_SFLOAT_S8_UINT

5.2 清除与加载策略

在渲染开始时清除深度缓冲是必要的:

VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = framebuffer;
renderPassInfo.renderArea = {{0, 0}, {width, height}};

VkClearValue clearValues[2]{};
clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
clearValues[1].depthStencil = {1.0f, 0}; // depth=1.0(最远)

renderPassInfo.clearValueCount = 2;
renderPassInfo.pClearValues = clearValues;

vkCmdBeginRenderPass(cmd, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);

对于延迟渲染G-Buffer,若每帧都会完全覆盖所有像素,颜色附件可使用LOAD_OP_DONT_CARE省去清除开销。但深度附件由于Early-Z测试的存在,必须明确清除,否则未写入的区域会保留错误深度值导致可见性判断异常。

六、MSAA 与 Resolve 机制

多重采样抗锯齿(MSAA)通过在单个像素内采集多个样本来减少几何边缘的锯齿。

6.1 MSAA Image 配置

第一步是修改颜色附件的采样数:

VkImageCreateInfo msaaColorInfo{};
msaaColorInfo.samples = VK_SAMPLE_COUNT_4_BIT; // 4x MSAA
msaaColorInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
vkCreateImage(device, &msaaColorInfo, nullptr, &msaaColorImage);

同时Pipeline的Rasterization State也要同步开启:

VkPipelineMultisampleStateCreateInfo multisampling{};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_TRUE; // 启用样本着色
multisampling.minSampleShading = 0.2f; // 至少20%的样本执行片段着色
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_4_BIT;

sampleShadingEnable是可选优化:开启后片段着色器会在每个样本而非每个像素执行,对于高频纹理细节有更好的效果,但代价是着色器调用次数成倍增加。

6.2 Resolve Subpass

MSAA渲染结果不能直输出到屏幕,必须通过Resolve操作将多样本降采样为单样本图像:

VkAttachmentDescription colorAttachmentResolve{};
colorAttachmentResolve.format = swapChainImageFormat;
colorAttachmentResolve.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachmentResolve.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachmentResolve.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachmentResolve.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

VkAttachmentReference colorAttachmentResolveRef{};
colorAttachmentResolveRef.attachment = 2;
colorAttachmentResolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

// 在Subpass中声明Resolve附件
subpass.pResolveAttachments = &colorAttachmentResolveRef;

Render Pass会自动在Subpass结束时执行Resolve,将pColorAttachments中的MSAA附件解析到pResolveAttachments对应的单样本附件上。对于深度缓冲通常不需要Resolve,因为它只用于渲染过程中的深度测试,不对外输出。

手动Resolve也是可行的,通过vkCmdResolveImage命令将MSAA Image解析到单样本 Image:

VkImageResolve resolveRegion{};
resolveRegion.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
resolveRegion.srcOffset = {0, 0, 0};
resolveRegion.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
resolveRegion.dstOffset = {0, 0, 0};
resolveRegion.extent = {width, height, 1};

vkCmdResolveImage(cmd,
    msaaColorImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
    resolveImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
    1, &resolveRegion);

七、VK_KHR_dynamic_rendering 动态渲染

Vulkan 1.3将VK_KHR_dynamic_rendering提升为核心功能,这是一项革命性的改进,它允许无需创建Render Pass和Framebuffer即可直接渲染到附件。

7.1 启用与链式调用

在设备创建时启用特性:

VkPhysicalDeviceDynamicRenderingFeatures dynamicRendering{};
dynamicRendering.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES;
dynamicRendering.dynamicRendering = VK_TRUE;

VkDeviceCreateInfo deviceCreateInfo{};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.pNext = &dynamicRendering;

7.2 动态渲染命令

使用vkCmdBeginRendering直接在命令缓冲中指定渲染附件:

VkRenderingAttachmentInfo colorAttachmentInfo{};
colorAttachmentInfo.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
colorAttachmentInfo.imageView = swapChainImageViews[imageIndex];
colorAttachmentInfo.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
colorAttachmentInfo.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachmentInfo.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachmentInfo.clearValue.color = {{0.0f, 0.0f, 0.0f, 1.0f}};

VkRenderingAttachmentInfo depthAttachmentInfo{};
depthAttachmentInfo.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
depthAttachmentInfo.imageView = depthImageView;
depthAttachmentInfo.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
depthAttachmentInfo.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthAttachmentInfo.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttachmentInfo.clearValue.depthStencil = {1.0f, 0};

VkRenderingInfo renderingInfo{};
renderingInfo.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
renderingInfo.renderArea = {{0, 0}, {width, height}};
renderingInfo.layerCount = 1;
renderingInfo.colorAttachmentCount = 1;
renderingInfo.pColorAttachments = &colorAttachmentInfo;
renderingInfo.pDepthAttachment = &depthAttachmentInfo;

vkCmdBeginRendering(cmd, &renderingInfo);
// ... 记录绘制命令 ...
vkCmdEndRendering(cmd);

动态渲染避免了繁琐的Render Pass兼容性检查,大幅简化了渲染器架构。特别是在实现Bindless渲染管线或动态光照系统时,不同相机视角或光照Pass可以使用不同的附件组合,无需预先创建大量Render Pass对象。

后续仍需要通过Image Memory Barrier将Attachment的layout从VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL转换到VK_IMAGE_LAYOUT_PRESENT_SRC_KHR以提交到交换链。

八、纹理加载的完整流程

从磁盘文件到GPU可采样纹理,中间涉及CPU解码、Staging Buffer中转、命令提交和布局转换多个环节。

8.1 完整加载管线

void loadTexture(const std::string& path, VkImage& outImage,
                 VkDeviceMemory& outMemory, VkImageView& outView) {
    // 1. 加载并解码图像文件(stb_image 等)
    int texWidth, texHeight, texChannels;
    stbi_uc* pixels = stbi_load(path.c_str(), &texWidth, &texHeight,
                                &texChannels, STBI_rgb_alpha);
    VkDeviceSize imageSize = texWidth * texHeight * 4;
    
    // 2. 创建Staging Buffer并上传像素数据
    VkBuffer stagingBuffer;
    VkDeviceMemory stagingMemory;
    createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
                 | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
                 stagingBuffer, stagingMemory);
    
    void* data;
    vkMapMemory(device, stagingMemory, 0, imageSize, 0, &data);
    memcpy(data, pixels, static_cast<size_t>(imageSize));
    vkUnmapMemory(device, stagingMemory);
    stbi_image_free(pixels);
    
    // 3. 创建目标Image(Device Local)
    createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_SRGB,
                VK_IMAGE_TILING_OPTIMAL,
                VK_IMAGE_USAGE_TRANSFER_DST_BIT
                | VK_IMAGE_USAGE_SAMPLED_BIT
                | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
                VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                outImage, outMemory);
    
    // 4. 布局转换:UNDEFINED -> TRANSFER_DST
    transitionImageLayout(outImage, VK_FORMAT_R8G8B8A8_SRGB,
        VK_IMAGE_LAYOUT_UNDEFINED,
        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
    
    // 5. 从Staging Buffer复制到Image
    copyBufferToImage(stagingBuffer, outImage,
        static_cast<uint32_t>(texWidth),
        static_cast<uint32_t>(texHeight));
    
    // 6. 生成Mipmap链
    generateMipmaps(outImage, VK_FORMAT_R8G8B8A8_SRGB,
        texWidth, texHeight, mipLevels);
    // generateMipmaps内部会自动执行TRANSFER_DST -> SHADER_READ_ONLY的转换
    
    // 7. 清理Staging资源
    vkDestroyBuffer(device, stagingBuffer, nullptr);
    vkFreeMemory(device, stagingMemory, nullptr);
    
    // 8. 创建ImageView和Sampler
    outView = createImageView(outImage, VK_FORMAT_R8G8B8A8_SRGB,
                              VK_IMAGE_ASPECT_COLOR_BIT, mipLevels);
}

8.2 布局转换的Barrier实现

void transitionImageLayout(VkImage image, VkFormat format,
    VkImageLayout oldLayout, VkImageLayout newLayout,
    uint32_t mipLevels) {
    VkImageMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.oldLayout = oldLayout;
    barrier.newLayout = newLayout;
    barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.image = image;
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    barrier.subresourceRange.baseMipLevel = 0;
    barrier.subresourceRange.levelCount = mipLevels;
    barrier.subresourceRange.baseArrayLayer = 0;
    barrier.subresourceRange.layerCount = 1;
    
    VkPipelineStageFlags sourceStage;
    VkPipelineStageFlags destinationStage;
    
    if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED
        && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
        barrier.srcAccessMask = 0;
        barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
        destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
               && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
        barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
        sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
        destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    }
    
    vkCmdPipelineBarrier(cmd,
        sourceStage, destinationStage,
        0, 0, nullptr, 0, nullptr, 1, &barrier);
}

8.3 生成 Mipmap

void generateMipmaps(VkImage image, VkFormat format,
    int32_t texWidth, int32_t texHeight, uint32_t mipLevels) {
    VkImageMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.image = image;
    barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    barrier.subresourceRange.baseArrayLayer = 0;
    barrier.subresourceRange.layerCount = 1;
    barrier.subresourceRange.levelCount = 1;
    
    int32_t mipWidth = texWidth;
    int32_t mipHeight = texHeight;
    
    for (uint32_t i = 1; i < mipLevels; i++) {
        // 等待前一级mipmap写入完成
        barrier.subresourceRange.baseMipLevel = i - 1;
        barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
        barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
        barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
        
        vkCmdPipelineBarrier(cmd,
            VK_PIPELINE_STAGE_TRANSFER_BIT,
            VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
            0, nullptr, 0, nullptr, 1, &barrier);
        
        // 执行Blit进行尺寸减半
        VkImageBlit blit{};
        blit.srcOffsets[0] = {0, 0, 0};
        blit.srcOffsets[1] = {mipWidth, mipHeight, 1};
        blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        blit.srcSubresource.mipLevel = i - 1;
        blit.srcSubresource.baseArrayLayer = 0;
        blit.srcSubresource.layerCount = 1;
        blit.dstOffsets[0] = {0, 0, 0};
        blit.dstOffsets[1] = {mipWidth > 1 ? mipWidth / 2 : 1,
                              mipHeight > 1 ? mipHeight / 2 : 1, 1};
        blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        blit.dstSubresource.mipLevel = i;
        blit.dstSubresource.baseArrayLayer = 0;
        blit.dstSubresource.layerCount = 1;
        
        vkCmdBlitImage(cmd,
            image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
            image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
            1, &blit, VK_FILTER_LINEAR);
        
        // 转换前一级为Shader可读
        barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
        barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
        barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
        barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
        
        vkCmdPipelineBarrier(cmd,
            VK_PIPELINE_STAGE_TRANSFER_BIT,
            VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
            0, nullptr, 0, nullptr, 1, &barrier);
        
        if (mipWidth > 1) mipWidth /= 2;
        if (mipHeight > 1) mipHeight /= 2;
    }
    
    // 转换最后一级mipmap
    barrier.subresourceRange.baseMipLevel = mipLevels - 1;
    barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
    
    vkCmdPipelineBarrier(cmd,
        VK_PIPELINE_STAGE_TRANSFER_BIT,
        VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
        0, nullptr, 0, nullptr, 1, &barrier);
}

九、小结

Vulkan的图像子系统以其显式控制著称,每一个对象、每一次布局转换都需要开发者精心设计。Image对象定义了内存分配的基本形态,ImageView提供了多维的访问视角,Sampler封装了硬件采样策略,而Framebuffer与Render Pass则将这一切编织进渲染管线。MSAA和Depth Buffer的配置直接影响画面质量与性能平衡,Dynamic Rendering则代表了现代Vulkan向着更低开销、更灵活架构演进的趋势。深入理解这些组件的协作关系,才能在实际工程中游刃有余地驾驭GPU资源。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Graphics」更多文章

  1. Vulkan 命令缓冲与同步机制深度解析