图像
一、 添加纹理的核心步骤总结
-
创建图像对象
- 由设备内存支持,区别于交换链自动创建的图像,需手动创建。
- 流程类似创建顶点缓冲:先创建暂存资源(或缓冲区)填充像素数据,再复制到最终图像。
-
填充图像像素
- 方案:通过VkBuffer存储像素数据,利用
vkCmdCopyBufferToImage
复制到图像(部分硬件性能更优)。
- 方案:通过VkBuffer存储像素数据,利用
-
创建图像采样器
- 用于定义纹理采样方式(如滤波、寻址模式等)。
-
添加描述符
- 组合图像采样器描述符,供着色器从纹理中采样颜色。
二、图像布局与关键概念
- 图像布局的重要性:
像素在内存中的组织方式影响硬件性能,操作图像时需确保布局匹配用途。 - 常见图像布局类型:
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
:适用于图像呈现。VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
:适用于作为片段着色器的颜色附件。VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
:适用于作为传输操作的源(如复制到缓冲区)。VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
:适用于作为传输操作的目标(如从缓冲区复制)。VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
:适用于着色器采样。
三、管线屏障的作用
- 布局转换:通过管线屏障(Pipeline Barrier)同步资源访问,转换图像布局(如从传输目标转为着色器可读)。
- 队列族所有权转移:当使用
VK_SHARING_MODE_EXCLUSIVE
时,屏障可用于转移资源的队列族所有权,确保多队列操作的正确性。
四、图像加载与暂存缓冲区创建
1. 使用 stbi 库加载图像
- 通过
stbi_load
函数读取纹理文件,强制加载为RGBA格式(STBI_rgb_alpha
),确保兼容性。 - 示例代码:
int texWidth, texHeight, texChannels;
stbi_uc* pixels = stbi_load("assets/textures/orange.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
2. 创建暂存缓冲区(Staging Buffer)
- 用途:在主机可见内存中存储像素数据,便于复制到Vulkan图像。
- 关键参数:
VK_BUFFER_USAGE_TRANSFER_SRC_BIT
:作为传输源缓冲区。VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
:主机可映射且一致的内存。
- 流程:
- 调用
createBuffer
函数创建缓冲区。 - 使用
vkMapMemory
映射内存,通过memcpy
复制像素数据。 - 调用
vkUnmapMemory
释放映射,并释放stbi加载的像素数组
- 调用
五、纹理图像创建与初始化
-
创建VkImage对象
- 通过
VkImageCreateInfo
结构体配置图像参数:- 图像类型:
VK_IMAGE_TYPE_2D
(二维纹理)。 - 格式:
VK_FORMAT_R8G8B8A8_SRGB
(RGBA格式,sRGB色彩空间)。 - 平铺模式:
VK_IMAGE_TILING_OPTIMAL
(硬件优化布局,提升访问效率)。 - 用法:
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT
(作为传输目标和着色器采样源)。
- 图像类型:
- 代码抽象:通过
createImage
函数封装图像创建逻辑,支持参数化配置。
- 通过
-
分配图像内存
- 流程:
- 调用
vkGetImageMemoryRequirements
查询内存需求。 - 使用
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
分配设备本地内存,提升性能。 - 通过
vkBindImageMemory
绑定内存到图像。
- 调用
- 流程:
六、图像布局转换与管线屏障
-
图像布局(Image Layout)核心概念
- 关键布局类型:
VK_IMAGE_LAYOUT_UNDEFINED
:初始未定义布局,首次转换可丢弃数据。VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
:适合作为缓冲区复制目标。VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
:适合着色器采样。
- 布局转换必须性:硬件要求操作图像时布局匹配,否则可能导致错误或性能下降。
- 关键布局类型:
-
管线屏障(Pipeline Barrier)实现布局转换
- 通过
vkCmdPipelineBarrier
函数同步资源访问并转换布局:- 参数配置:
srcAccessMask
/dstAccessMask
:指定屏障前后的访问权限(如传输写入、着色器读取)。sourceStage
/destinationStage
:指定管线阶段(如传输阶段、片段着色器阶段)。
- 典型转换场景:
- 从
UNDEFINED
到TRANSFER_DST_OPTIMAL
:为缓冲区复制准备图像。 - 从
TRANSFER_DST_OPTIMAL
到SHADER_READ_ONLY_OPTIMAL
:为着色器采样准备图像。
- 从
- 参数配置:
- 通过
七、缓冲区到图像的复制操作
- 使用
copyBufferToImage
函数- 核心步骤:
- 创建命令缓冲区,记录复制操作。
- 通过
VkBufferImageCopy
结构体指定复制区域:bufferOffset
:缓冲区偏移量。imageExtent
:图像目标区域尺寸。
- 调用
vkCmdCopyBufferToImage
执行复制,需指定目标图像的当前布局(如TRANSFER_DST_OPTIMAL
)。
- 核心步骤:
八、辅助函数与资源管理
-
命令缓冲区辅助函数
beginSingleTimeCommands
/endSingleTimeCommands
:封装单次提交命令缓冲区的流程,简化异步操作。copyBuffer
:封装缓冲区复制逻辑,支持通用缓冲区复制场景。
-
资源清理
- 释放暂存缓冲区:
vkDestroyBuffer
+vkFreeMemory
。 - 释放纹理图像:
vkDestroyImage
+vkFreeMemory
。
- 释放暂存缓冲区:
九、关键注意事项
- 格式兼容性:确保图像格式(如
VK_FORMAT_R8G8B8A8_SRGB
)被硬件支持,否则需备选方案。 - 验证层提示:布局转换时需正确设置访问掩码和管线阶段,避免验证层报错。
- 性能优化:
- 使用
VK_IMAGE_TILING_OPTIMAL
提升硬件访问效率。 - 设备本地内存(
DEVICE_LOCAL_BIT
)适合频繁访问的图像资源。
- 使用
十、代码结构优化
- 函数抽象:将重复逻辑(如图像创建、命令缓冲区管理)封装为工具函数,提升代码可维护性。
- 异步执行:实际应用中可合并多个命令缓冲区批量提交,减少队列等待开销。
图像视图与采样器实现总结
一、图像视图(Image View)
-
概念与作用
- 图像视图是对图像的一种“视图”抽象,定义了如何访问图像数据(如格式、维度、子资源范围)。
- 所有图像(包括纹理和交换链图像)必须通过图像视图访问。
-
创建流程
- 关键参数:
image
:关联的VkImage对象。viewType
:视图类型(如VK_IMAGE_VIEW_TYPE_2D
)。format
:数据格式(如VK_FORMAT_R8G8B8A8_SRGB
)。subresourceRange
:指定访问的Mip级别和数组层(通常为全范围)。
- 代码示例:
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; // 设置Mip级别和数组层范围... vkCreateImageView(device, &viewInfo, nullptr, &textureImageView);
- 关键参数:
-
代码优化
- 抽象通用函数
createImageView
,减少重复代码:VkImageView createImageView(VkImage image, VkFormat format) { // 初始化viewInfo... vkCreateImageView(device, &viewInfo, nullptr, &imageView); return imageView; }
- 抽象通用函数
二、采样器(Sampler)
-
概念与作用
- 采样器定义了如何从纹理中提取纹素值,处理过滤、寻址模式和Mipmapping等操作。
- 与图像视图分离,可独立配置并应用于多个图像。
-
核心配置参数
- 过滤模式:
magFilter
/minFilter
:放大/缩小时的插值方式(VK_FILTER_NEAREST
或VK_FILTER_LINEAR
)。
- 寻址模式(U/V/W轴):
VK_SAMPLER_ADDRESS_MODE_REPEAT
:超出边界时重复纹理。VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE
:使用边缘颜色。- 其他模式:
MIRRORED_REPEAT
、CLAMP_TO_BORDER
等。
- 各向异性过滤:
anisotropyEnable
:是否启用。maxAnisotropy
:最大采样数(需查询设备支持的最大值)。
- Mipmapping参数:
mipmapMode
:Mip级别插值方式(如VK_SAMPLER_MIPMAP_MODE_LINEAR
)。minLod
/maxLod
:允许的最小/最大Lod值。
- 过滤模式:
-
创建流程
- 查询设备支持:
VkPhysicalDeviceProperties properties{}; vkGetPhysicalDeviceProperties(physicalDevice, &properties); samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy;
- 初始化采样器信息:
VkSamplerCreateInfo samplerInfo{}; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; // 设置过滤、寻址、各向异性等参数... vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler);
- 查询设备支持:
三、设备特性与验证层
-
启用各向异性过滤
- 在创建逻辑设备时请求特性:
VkPhysicalDeviceFeatures deviceFeatures{}; deviceFeatures.samplerAnisotropy = VK_TRUE; vkCreateDevice(physicalDevice, &createInfo, nullptr, &device);
- 验证设备支持:
bool isDeviceSuitable(VkPhysicalDevice device) { VkPhysicalDeviceFeatures supportedFeatures; vkGetPhysicalDeviceFeatures(device, &supportedFeatures); return supportedFeatures.samplerAnisotropy; // 检查各向异性支持 }
- 在创建逻辑设备时请求特性:
-
验证层常见问题
- 未启用各向异性特性时,验证层会报错。
- 解决方案:强制要求特性或有条件禁用各向异性。
四、资源管理
-
创建顺序:
initVulkan() { createTextureImage(); // 创建纹理图像 createTextureImageView(); // 创建图像视图 createTextureSampler(); // 创建采样器 }
-
销毁顺序:
cleanup() { vkDestroySampler(device, textureSampler, nullptr); vkDestroyImageView(device, textureImageView, nullptr); vkDestroyImage(device, textureImage, nullptr); vkFreeMemory(device, textureImageMemory, nullptr); }
五、关键优化与注意事项
-
性能优化:
- 启用各向异性过滤可提升纹理质量,尤其在斜视角下。
- 使用线性过滤(
VK_FILTER_LINEAR
)减少锯齿,但会增加计算开销。
-
兼容性考虑:
- 检查设备对各向异性过滤的支持,避免运行时错误。
- 格式一致性:图像视图格式需与图像格式匹配。
-
代码复用:
- 抽象通用函数(如
createImageView
),降低维护成本。
- 抽象通用函数(如
组合图像采样器实现总结
一、描述符更新与资源绑定
-
组合图像采样器描述符类型
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
:用于将图像视图与采样器组合,供着色器访问纹理。- 描述符集布局配置:
VkDescriptorSetLayoutBinding samplerLayoutBinding{}; samplerLayoutBinding.binding = 1; // 绑定位置 samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; // 仅在片段着色器使用
-
描述符池扩展
- 新增组合图像采样器的描述符类型支持:
VkDescriptorPoolSize poolSizes[] = { {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_FRAMES_IN_FLIGHT}, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_FRAMES_IN_FLIGHT} };
- 新增组合图像采样器的描述符类型支持:
-
资源绑定到描述符集
- 使用
VkDescriptorImageInfo
关联图像视图与采样器:VkDescriptorImageInfo imageInfo{}; imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; imageInfo.imageView = textureImageView; imageInfo.sampler = textureSampler;
- 通过
vkUpdateDescriptorSets
更新描述符集:VkWriteDescriptorSet descriptorWrite{}; descriptorWrite.dstBinding = 1; // 对应绑定位置 descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; descriptorWrite.pImageInfo = &imageInfo;
- 使用
二、纹理坐标与顶点结构
-
顶点结构扩展
- 添加纹理坐标
texCoord
字段(UV坐标):struct Vertex { glm::vec2 pos; // 顶点位置 glm::vec3 color; // 顶点颜色 glm::vec2 texCoord; // 纹理坐标 };
- 顶点属性描述更新:
attributeDescriptions[2].location = 2; attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT; attributeDescriptions[2].offset = offsetof(Vertex, texCoord);
- 添加纹理坐标
-
顶点数据示例
- 为四边形顶点分配纹理坐标(范围[0,1]):
{{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}}, // 左下角对应纹理(1,0) {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}}, // 右下角对应纹理(0,0) {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}}, // 右上角对应纹理(0,1) {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}} // 左上角对应纹理(1,1)
- 为四边形顶点分配纹理坐标(范围[0,1]):
三、着色器修改与纹理采样
-
顶点着色器调整
- 输入纹理坐标并传递给片段着色器:
layout(location = 2) in vec2 inTexCoord; // 输入顶点纹理坐标 layout(location = 1) out vec2 fragTexCoord; // 输出插值后的纹理坐标 void main() { fragTexCoord = inTexCoord; // 光栅化器自动插值 }
- 输入纹理坐标并传递给片段着色器:
-
片段着色器实现纹理采样
- 声明采样器并使用
texture
函数采样:layout(binding = 1) uniform sampler2D texSampler; // 对应描述符绑定位置 void main() { outColor = texture(texSampler, fragTexCoord); // 基于插值后的纹理坐标采样 }
- 纹理坐标可视化(调试用):
outColor = vec4(fragTexCoord, 0.0, 1.0); // 用RGB通道显示UV坐标
- 顶点颜色与纹理混合:
outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0);
- 声明采样器并使用
四、关键注意事项
-
描述符池容量
- 确保描述符池大小足够,避免
VK_ERROR_POOL_OUT_OF_MEMORY
错误。 - 最佳实践:按
MAX_FRAMES_IN_FLIGHT
设置描述符数量,匹配帧缓冲数量。
- 确保描述符池大小足够,避免
-
纹理坐标与寻址模式
- 归一化坐标范围[0,1],超出范围时由采样器寻址模式决定行为(如重复、镜像、 clamping)。
- 示例:
fragTexCoord * 2.0
可测试VK_SAMPLER_ADDRESS_MODE_REPEAT
平铺效果。
-
验证层与设备兼容性
- 描述符池分配不足时,部分驱动可能静默处理,但验证层可能警告。
- 确保设备支持组合图像采样器相关特性(现代显卡通常支持)。
代码更新
笔记主要参考官方教程《Vulkan Tutorial》写的,实现代码自主重构,目标是完成一个实用的 Vulkan 渲染器。
- VkContext 新增成员变量
struct VkContext {
...
VkImage textureImage;
VkDeviceMemory textureImageMemory;
VkImageView textureImageView;
VkSampler textureSampler;
}
- VkTypes 更新 Vertex 结构
struct Vertex {
glm::vec2 pos;
glm::vec3 color;
glm::vec2 texCoord; // 新增纹理坐标属性
...
// 扩充容量,新增纹理属性描述符
static std::array<VkVertexInputAttributeDescription, 3> getAttributeDescriptions() {
std::array<VkVertexInputAttributeDescription, 3> attributeDescriptions{};
...
attributeDescriptions[2].binding = 0;
attributeDescriptions[2].location = 2;
attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT;
attributeDescriptions[2].offset = offsetof(Vertex, texCoord);
return attributeDescriptions;
}
};
- HelloRect 更新纹理坐标
#include "HelloRect.h"
#include <chrono>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
HelloRect::HelloRect()
// 顶点位置 顶点颜色 纹理坐标
: 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}}})
, indices({0, 1, 2, 2, 3, 0})
, ubo({}) {}
...
- VkInit 中添加纹理和采样器代码
...
// 创建纹理
{
// 定义变量存储纹理的宽度、高度和通道数
int texWidth, texHeight, texChannels;
// 使用stb_image库加载纹理图片,强制转换为RGBA格式
stbi_uc* pixels =
stbi_load("assets/textures/orange.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
VkDeviceSize imageSize = texWidth * texHeight * 4; // 计算图像数据大小
// 检查图片是否成功加载
if (!pixels) {
throw std::runtime_error("加载纹理图片失败!");
}
// 创建暂存缓冲区,用于在CPU和GPU之间传输图像数据
VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;
// 调用辅助函数创建暂存缓冲区
createBuffer(vkcontext->physicalDevice,
vkcontext->device,
imageSize,
VK_BUFFER_USAGE_TRANSFER_SRC_BIT, // 用作传输源
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, // CPU可访问且内存连贯
stagingBuffer,
stagingBufferMemory);
// 将图像数据从CPU复制到暂存缓冲区
void* data;
vkMapMemory(vkcontext->device, stagingBufferMemory, 0, imageSize, 0, &data);
memcpy(data, pixels, static_cast<size_t>(imageSize));
vkUnmapMemory(vkcontext->device, stagingBufferMemory);
stbi_image_free(pixels); // 释放不再需要的原始图像数据
// 创建实际的设备端图像
createImage(vkcontext->physicalDevice,
vkcontext->device,
texWidth,
texHeight,
VK_FORMAT_R8G8B8A8_SRGB, // RGBA 8位格式
VK_IMAGE_TILING_OPTIMAL, // 设备优化的内存布局
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, // 用作传输目标和着色器采样源
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, // 设备本地内存(GPU可快速访问)
vkcontext->textureImage,
vkcontext->textureImageMemory);
// 执行三次图像布局转换和数据复制操作:
// 1. 将图像布局从未定义转换为传输目标优化布局
transitionImageLayout(vkcontext->textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, vkcontext);
// 2. 将暂存缓冲区中的数据复制到图像
copyBufferToImage(stagingBuffer, vkcontext->textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight), vkcontext);
// 3. 将图像布局从传输目标优化布局转换为着色器只读优化布局
transitionImageLayout(vkcontext->textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, vkcontext);
// 释放不再需要的暂存缓冲区资源
vkDestroyBuffer(vkcontext->device, stagingBuffer, nullptr);
vkFreeMemory(vkcontext->device, stagingBufferMemory, nullptr);
}
// 创建纹理图像视图,定义如何访问图像数据
vkcontext->textureImageView = createImageView(vkcontext->textureImage, VK_FORMAT_R8G8B8A8_SRGB, vkcontext->device);
// 创建纹理采样器,定义如何从纹理中采样颜色
{
// 获取物理设备属性,用于查询各方面限制
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(vkcontext->physicalDevice, &properties);
// 配置采样器参数
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; // U方向纹理坐标超出范围时重复纹理
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; // V方向纹理坐标超出范围时重复纹理
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; // W方向纹理坐标超出范围时重复纹理
samplerInfo.anisotropyEnable = VK_TRUE; // 启用各向异性过滤
samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy; // 使用设备支持的最大各向异性级别
samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; // 纹理坐标超出范围时的边界颜色
samplerInfo.unnormalizedCoordinates = VK_FALSE; // 使用归一化纹理坐标
samplerInfo.compareEnable = VK_FALSE; // 不启用纹理比较功能
samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; // 比较操作(当compareEnable为VK_TRUE时有效)
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; // 使用线性过滤选择mip级别
// 创建采样器
if (vkCreateSampler(vkcontext->device, &samplerInfo, nullptr, &vkcontext->textureSampler) != VK_SUCCESS) {
throw std::runtime_error("创建纹理采样器失败!");
}
}
...
编译构建,最终效果:
代码分支: 09_textures