纹理与帧缓冲是图形渲染管线中最基础也最关键的两个数据载体。理解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_UNDEFINED或VK_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_1D、VK_IMAGE_VIEW_TYPE_2D、VK_IMAGE_VIEW_TYPE_3D、VK_IMAGE_VIEW_TYPE_CUBE(CubeMap)、VK_IMAGE_VIEW_TYPE_1D_ARRAY、VK_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);
上述compareEnable和compareOp用于阴影贴图的百分比接近过滤(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_UINT或VK_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资源。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。