Vulkan学习笔记11—纹理映射

图像

一、 添加纹理的核心步骤总结

  1. 创建图像对象

    • 由设备内存支持,区别于交换链自动创建的图像,需手动创建。
    • 流程类似创建顶点缓冲:先创建暂存资源(或缓冲区)填充像素数据,再复制到最终图像。
  2. 填充图像像素

    • 方案:通过VkBuffer存储像素数据,利用vkCmdCopyBufferToImage复制到图像(部分硬件性能更优)。
  3. 创建图像采样器

    • 用于定义纹理采样方式(如滤波、寻址模式等)。
  4. 添加描述符

    • 组合图像采样器描述符,供着色器从纹理中采样颜色。

二、图像布局与关键概念

  • 图像布局的重要性
    像素在内存中的组织方式影响硬件性能,操作图像时需确保布局匹配用途。
  • 常见图像布局类型
    • 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加载的像素数组

五、纹理图像创建与初始化

  1. 创建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函数封装图像创建逻辑,支持参数化配置。
  2. 分配图像内存

    • 流程:
      1. 调用vkGetImageMemoryRequirements查询内存需求。
      2. 使用VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT分配设备本地内存,提升性能。
      3. 通过vkBindImageMemory绑定内存到图像。

六、图像布局转换与管线屏障

  1. 图像布局(Image Layout)核心概念

    • 关键布局类型
      • VK_IMAGE_LAYOUT_UNDEFINED:初始未定义布局,首次转换可丢弃数据。
      • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:适合作为缓冲区复制目标。
      • VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:适合着色器采样。
    • 布局转换必须性:硬件要求操作图像时布局匹配,否则可能导致错误或性能下降。
  2. 管线屏障(Pipeline Barrier)实现布局转换

    • 通过vkCmdPipelineBarrier函数同步资源访问并转换布局:
      • 参数配置
        • srcAccessMask/dstAccessMask:指定屏障前后的访问权限(如传输写入、着色器读取)。
        • sourceStage/destinationStage:指定管线阶段(如传输阶段、片段着色器阶段)。
      • 典型转换场景
        1. UNDEFINEDTRANSFER_DST_OPTIMAL:为缓冲区复制准备图像。
        2. TRANSFER_DST_OPTIMALSHADER_READ_ONLY_OPTIMAL:为着色器采样准备图像。

七、缓冲区到图像的复制操作

  1. 使用copyBufferToImage函数
    • 核心步骤:
      • 创建命令缓冲区,记录复制操作。
      • 通过VkBufferImageCopy结构体指定复制区域:
        • bufferOffset:缓冲区偏移量。
        • imageExtent:图像目标区域尺寸。
      • 调用vkCmdCopyBufferToImage执行复制,需指定目标图像的当前布局(如TRANSFER_DST_OPTIMAL)。

八、辅助函数与资源管理

  1. 命令缓冲区辅助函数

    • beginSingleTimeCommands/endSingleTimeCommands:封装单次提交命令缓冲区的流程,简化异步操作。
    • copyBuffer:封装缓冲区复制逻辑,支持通用缓冲区复制场景。
  2. 资源清理

    • 释放暂存缓冲区:vkDestroyBuffer + vkFreeMemory
    • 释放纹理图像:vkDestroyImage + vkFreeMemory

九、关键注意事项

  1. 格式兼容性:确保图像格式(如VK_FORMAT_R8G8B8A8_SRGB)被硬件支持,否则需备选方案。
  2. 验证层提示:布局转换时需正确设置访问掩码和管线阶段,避免验证层报错。
  3. 性能优化
    • 使用VK_IMAGE_TILING_OPTIMAL提升硬件访问效率。
    • 设备本地内存(DEVICE_LOCAL_BIT)适合频繁访问的图像资源。

十、代码结构优化

  • 函数抽象:将重复逻辑(如图像创建、命令缓冲区管理)封装为工具函数,提升代码可维护性。
  • 异步执行:实际应用中可合并多个命令缓冲区批量提交,减少队列等待开销。

图像视图与采样器实现总结

一、图像视图(Image View)

  1. 概念与作用

    • 图像视图是对图像的一种“视图”抽象,定义了如何访问图像数据(如格式、维度、子资源范围)。
    • 所有图像(包括纹理和交换链图像)必须通过图像视图访问。
  2. 创建流程

    • 关键参数
      • 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);
      
  3. 代码优化

    • 抽象通用函数createImageView,减少重复代码:
      VkImageView createImageView(VkImage image, VkFormat format) {
          // 初始化viewInfo...
          vkCreateImageView(device, &viewInfo, nullptr, &imageView);
          return imageView;
      }
      

二、采样器(Sampler)

  1. 概念与作用

    • 采样器定义了如何从纹理中提取纹素值,处理过滤、寻址模式和Mipmapping等操作。
    • 与图像视图分离,可独立配置并应用于多个图像。
  2. 核心配置参数

    • 过滤模式
      • magFilter/minFilter:放大/缩小时的插值方式(VK_FILTER_NEARESTVK_FILTER_LINEAR)。
    • 寻址模式(U/V/W轴):
      • VK_SAMPLER_ADDRESS_MODE_REPEAT:超出边界时重复纹理。
      • VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE:使用边缘颜色。
      • 其他模式:MIRRORED_REPEATCLAMP_TO_BORDER等。
    • 各向异性过滤
      • anisotropyEnable:是否启用。
      • maxAnisotropy:最大采样数(需查询设备支持的最大值)。
    • Mipmapping参数
      • mipmapMode:Mip级别插值方式(如VK_SAMPLER_MIPMAP_MODE_LINEAR)。
      • minLod/maxLod:允许的最小/最大Lod值。
  3. 创建流程

    • 查询设备支持
      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);
      

三、设备特性与验证层

  1. 启用各向异性过滤

    • 在创建逻辑设备时请求特性:
      VkPhysicalDeviceFeatures deviceFeatures{};
      deviceFeatures.samplerAnisotropy = VK_TRUE;
      vkCreateDevice(physicalDevice, &createInfo, nullptr, &device);
      
    • 验证设备支持:
      bool isDeviceSuitable(VkPhysicalDevice device) {
          VkPhysicalDeviceFeatures supportedFeatures;
          vkGetPhysicalDeviceFeatures(device, &supportedFeatures);
          return supportedFeatures.samplerAnisotropy; // 检查各向异性支持
      }
      
  2. 验证层常见问题

    • 未启用各向异性特性时,验证层会报错。
    • 解决方案:强制要求特性或有条件禁用各向异性。

四、资源管理

  1. 创建顺序

    initVulkan() {
        createTextureImage();      // 创建纹理图像
        createTextureImageView();  // 创建图像视图
        createTextureSampler();    // 创建采样器
    }
    
  2. 销毁顺序

    cleanup() {
        vkDestroySampler(device, textureSampler, nullptr);
        vkDestroyImageView(device, textureImageView, nullptr);
        vkDestroyImage(device, textureImage, nullptr);
        vkFreeMemory(device, textureImageMemory, nullptr);
    }
    

五、关键优化与注意事项

  1. 性能优化

    • 启用各向异性过滤可提升纹理质量,尤其在斜视角下。
    • 使用线性过滤(VK_FILTER_LINEAR)减少锯齿,但会增加计算开销。
  2. 兼容性考虑

    • 检查设备对各向异性过滤的支持,避免运行时错误。
    • 格式一致性:图像视图格式需与图像格式匹配。
  3. 代码复用

    • 抽象通用函数(如createImageView),降低维护成本。

组合图像采样器实现总结

一、描述符更新与资源绑定

  1. 组合图像采样器描述符类型

    • 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;  // 仅在片段着色器使用
      
  2. 描述符池扩展

    • 新增组合图像采样器的描述符类型支持:
      VkDescriptorPoolSize poolSizes[] = {
          {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, MAX_FRAMES_IN_FLIGHT},
          {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_FRAMES_IN_FLIGHT}
      };
      
  3. 资源绑定到描述符集

    • 使用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;
      

二、纹理坐标与顶点结构

  1. 顶点结构扩展

    • 添加纹理坐标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);
      
  2. 顶点数据示例

    • 为四边形顶点分配纹理坐标(范围[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)
      

三、着色器修改与纹理采样

  1. 顶点着色器调整

    • 输入纹理坐标并传递给片段着色器:
      layout(location = 2) in vec2 inTexCoord;       // 输入顶点纹理坐标
      layout(location = 1) out vec2 fragTexCoord;    // 输出插值后的纹理坐标
      
      void main() {
          fragTexCoord = inTexCoord;  // 光栅化器自动插值
      }
      
  2. 片段着色器实现纹理采样

    • 声明采样器并使用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);
      

四、关键注意事项

  1. 描述符池容量

    • 确保描述符池大小足够,避免VK_ERROR_POOL_OUT_OF_MEMORY错误。
    • 最佳实践:按MAX_FRAMES_IN_FLIGHT设置描述符数量,匹配帧缓冲数量。
  2. 纹理坐标与寻址模式

    • 归一化坐标范围[0,1],超出范围时由采样器寻址模式决定行为(如重复、镜像、 clamping)。
    • 示例:fragTexCoord * 2.0可测试VK_SAMPLER_ADDRESS_MODE_REPEAT平铺效果。
  3. 验证层与设备兼容性

    • 描述符池分配不足时,部分驱动可能静默处理,但验证层可能警告。
    • 确保设备支持组合图像采样器相关特性(现代显卡通常支持)。

代码更新

笔记主要参考官方教程《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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值