Vulkan学习(四):Shader加载 & 管线设置

Graphics Pipeline

  下图中带星号的是可编程管线。

Created with Raphaël 2.3.0 Vertex/Index Buffer Input Assembler *Vertex Shader *Tessellation *Geometry Shader Rasterization *Fragment Shader Color Blending Frame Buffer

Input Assembler

  从指定的缓冲区中获取原始顶点数据,并且可以使用Index Buffer来重复使用某些元素,而不必复制顶点数据。

Vertex Shader

  每个网格顶点运行一次,并且为每个顶点执行变换(Transform)操作。

Tessellation Shader

  可根据某些规则细分三角形面片,以提高网格质量。

Geometry Shader

   每个图元(三角形,直线,点)执行一次,并且可以丢弃或输出比引入的图元更多的图元。现在程序中使用并不多,因为除英特尔的集显外,大多数显卡Geometry Shader性能都不太好。

Rasterization

  光栅化,图元离散为片段。也就是在帧缓冲区上填充像素。屏幕外部的所有片段都将被丢弃,并且顶点着色器输出的属性将在这些片段之间进行插值。 通常使用深度测试。

Fragment Shader

  每个保留的片段调用一次,确定将片段写入哪个帧缓冲区,以及使用哪个颜色和深度值。 它可以使用来自Vertex Shader的插值数据来执行此操作,如纹理坐标、法线等。

Color Blending

  混合映射到帧缓冲区中同一像素的不同片段。 片段可以根据透明度相互覆盖,累加或混合。

  OpenGL和Direct3D等API,是通过调用glBlendFunc和OMSetBlendState更改管线设置。 Vulkan中的图形管线是完全不变的,因此如果要更改着色器,绑定不同的帧缓冲区或更改混合功能,则必须重新创建管线。 缺点是必须创建许多管线,这些管线代表要在渲染操作中使用的状态的所有不同组合。 但是,由于预先知道管线中要执行的所有操作,因此驱动程序可以对其进行更好的优化。

Shader Modules

  与早期的API不同,Vulkan中的着色器代码与GLSL和HLSL不同,不使用编程语言,而是使用字节码(bytecode). 称为SPIR-V,可以同时被Vulkan和OpenCL(都是Khronos API)使用。 它是一种可用于Write Graphics和Compute Shaders的格式。使用字节码的优势是:

  • 降低GPU将Shader代码转换为本机代码的编译时间。
  • 由于不同GPU厂商将GLSL或HLSL转换为硬件支持的编译指令略有不用,可能导致相同的Shader代码在不用的硬件平台上出现编译错误。而使用bytecode可以避免类似的错误出现(SPIR-V)。

将GLSL或HLSL转换为SPIR-V的两种方式

  • 使用Vulkan自带的glslangValidator.exe或glslc.exe

    • Windows

      	C:/VulkanSDK/x.x.x.x/Bin32/glslc.exe shader.vert -o vert.spv
      	C:/VulkanSDK/x.x.x.x/Bin32/glslc.exe shader.frag -o frag.spv
      
    • Linux

      	/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.vert -o vert.spv
      	/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.frag -o frag.spv
      
  • Vulkan SDK包含libshaderc库,这是一个可以在程序内将GLSL代码转化为SPIR-V的库.

加载Shader

   shader的基础和OpenGL GLSL相同,不做笔记。

	#include <fstream>
	//ate: 从文件末尾开始阅读
	//binary: 读取文件为二进制文件 (避免文字转换)
	static std::vector<char> readFile(const std::string& filename) {
		std::ifstream file(filename, std::ios::ate | std::ios::binary);
		if (!file.is_open()) {
			throw std::runtime_error("failed to open file!");
		}
		//ate的优势是,可以获取文件的大小
		size_t fileSize = (size_t)file.tellg();
		std::vector<char> buffer(fileSize);
		//指针跳到头
		file.seekg(0);
		file.read(buffer.data(), fileSize);
		file.close();
		return buffer;
	}
	.............................................................................
	auto vertShaderCode = readFile("shaders/vert.spv");
	auto fragShaderCode = readFile("shaders/frag.spv");

Creating Shader Modules

  加载完Shader后,创建 Shader Module

	VkShaderModuleCreateInfo createInfo = {};
	createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
	createInfo.codeSize = code.size();
	createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());
	VkShaderModule shaderModule;
	if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
		throw std::runtime_error("failed to create shader module!");
	}

Shader Stage Creation

		auto vertShaderCode = readFile("shaders/vert.spv");
		auto fragShaderCode = readFile("shaders/frag.spv");
		VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
		VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);

		//可以将多个着色器组合到一个ShaderModule中,并使用不同的entry points来区分它们的行为。
		VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
		vertShaderStageInfo.sType =	VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
		vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
		vertShaderStageInfo.module = vertShaderModule;
		vertShaderStageInfo.pName = "main";
		//指定着色器常量的值。 
		//单个着色器模块,在管道中创建常量,给予不同的值来配置其行为。
		//比在渲染时使用变量配置着色器更为有效, 编译器可以进行优化,例如消除依赖于这些值的if语句
		//如果没有这样的常量,则可以将成员设置为nullptr,初始化会自动执行该操作。
		//pSpecializationInfo	

		VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
		fragShaderStageInfo.sType =	VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
		fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
		fragShaderStageInfo.module = fragShaderModule;
		fragShaderStageInfo.pName = "main";

		VkPipelineShaderStageCreateInfo shaderStages[] = { vertShaderStageInfo, fragShaderStageInfo };
		//在创建图形管线之前,不会将SPIR-V字节码编译并链接到机器代码以供GPU执行。
		//这意味着在管道创建完成后,就可以立即销毁ShaderModule
		vkDestroyShaderModule(device, fragShaderModule, nullptr);
		vkDestroyShaderModule(device, vertShaderModule, nullptr);

Fixed functions

Vertex input

   VkPipelineVertexInputStateCreateInfo描述传入到顶点着色器的顶点数据的格式。 有两种描述方式:
Bindings: 描述数据之间的间距, 以及数据是per-vertex还是按per-instance
Attribute descriptions: 传入到顶点着色器的属性类型,描述其在哪个偏移处绑定加载的

	VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
	vertexInputInfo.sType =	VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
	vertexInputInfo.vertexBindingDescriptionCount = 0;
	//描述加载顶点数据的详细信息
	vertexInputInfo.pVertexBindingDescriptions = nullptr; 
	vertexInputInfo.vertexAttributeDescriptionCount = 0;
	//描述加载顶点数据的详细信息
	vertexInputInfo.pVertexAttributeDescriptions = nullptr; 

Input assembly

   VkPipelineInputAssemblyStateCreateInfo描述两类信息:

  • 绘制什么样的几何图形
  • 是否启用图元重启( primitive restart)

具体值如下:

  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP

   通常,vertices是按顺序从顶点缓冲区按索引加载的,但是通过元素缓冲区,您可以指定要使用的索引。 这使您可以执行诸如重用顶点之类的优化。 如果您将primaryRestartEnable成员设置为VK_TRUE,则可以通过使用特殊索引0xFFFF或0xFFFFFFFF在_STRIP拓扑模式下分解直线和三角形。

Viewports and Scissors

   Viewport 描述图像渲染到帧缓冲区的区域。swap chain和images的大小可能与Viewport的宽高不同。

	//viewport
	VkViewport viewport = {};
	viewport.x = 0.0f;
	viewport.y = 0.0f;
	viewport.width = (float)swapChainExtent.width;
	viewport.height = (float)swapChainExtent.height;
	viewport.minDepth = 0.0f;
	viewport.maxDepth = 1.0f;

	//裁剪矩形定义哪些区域像素被存储
	VkRect2D scissor = {};
	scissor.offset = { 0, 0 };
	scissor.extent = swapChainExtent;

	//viewport 和scissor可以有多个
	VkPipelineViewportStateCreateInfo viewportState = {};
	viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
	viewportState.viewportCount = 1;
	viewportState.pViewports = &viewport;
	viewportState.scissorCount = 1;
	viewportState.pScissors = &scissor;

Rasterizer

   Rasterizer 从顶点着色器获取图形,并将其转换为片段,再由片段着色器着色。 它还执行深度测试,面剔除和剪裁测试,并且可以设置多边形填充还是线框渲染。

	VkPipelineRasterizationStateCreateInfo rasterizer = {};
	rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
	//保留近平面和远平面之外的片元
	rasterizer.depthClampEnable = VK_FALSE;
	//true 几何图形不会通过光栅化阶段。 禁用对帧缓冲区的任何输出
	rasterizer.rasterizerDiscardEnable = VK_FALSE;
	//VK_POLYGON_MODE_FILL 用片段填充多边形区域
	//VK_POLYGON_MODE_LINE 多边形边缘绘制为线
	//VK_POLYGON_MODE_POINT 多边形顶点绘制为点
	rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
	//支持的最大线宽取决于硬件,任何比1.0f粗的线都需要启用wideLines。
	rasterizer.lineWidth = 1.0f;
	//确定要使用的面部剔除类型。
	rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
	//指定面片视为正面的顶点顺序,可以是顺时针或逆时针
	rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
	//Rasterization可以通过添加常量或基于片段的斜率对深度值进行偏置来更改深度值。
	//多用于阴影贴图,如不需要将depthBiasEnable设置为VK_FALSE。
	rasterizer.depthBiasEnable = VK_FALSE;
	rasterizer.depthBiasConstantFactor = 0.0f; 
	rasterizer.depthBiasClamp = 0.0f; 
	rasterizer.depthBiasSlopeFactor = 0.0f; 

Multisampling

   MSAA与SSAA相似,在光栅化阶段,在一个像素区域内使用多个采样点,但每个像素内所有采样点共享一个着色计算,然后将颜色结果复制到每个采样点上。

	//后续补充
	VkPipelineMultisampleStateCreateInfo multisampling = {};
	multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
	multisampling.sampleShadingEnable = VK_FALSE;
	multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
	multisampling.minSampleShading = 1.0f;
	multisampling.pSampleMask = nullptr; 
	multisampling.alphaToCoverageEnable = VK_FALSE; 
	multisampling.alphaToOneEnable = VK_FALSE; 

Depth and stencil testing

   如果开启深度测试和模板测试,需要使用VkPipelineDepthStencilStateCreateInfo 后续补充。

Color blending

   片段着色器输出颜色后,需要将其与帧缓冲区中已经存在的颜色合并。

有两种方法可以实现:

  • 混合新旧值以产生最终颜色
  • 使用按位运算组合新旧值

每个附加的帧缓冲区的混合规则 :

	//specification详见说明文档
	VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
	colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT |
		VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT |
		VK_COLOR_COMPONENT_A_BIT;
	//片段着色器的颜色直接输出
	colorBlendAttachment.blendEnable = VK_FALSE;
	colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; 
	colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; 
	colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; 
	colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; 
	colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; 
	colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; 
	//or
	//VK_TRUE
	colorBlendAttachment.blendEnable = VK_TRUE;
	colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
	colorBlendAttachment.dstColorBlendFactor =	VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
	colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
	colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
	colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
	colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;

混合伪代码:

	if (blendEnable) {
		finalColor.rgb = (srcColorBlendFactor * newColor.rgb)
			< colorBlendOp > (dstColorBlendFactor * oldColor.rgb);
		finalColor.a = (srcAlphaBlendFactor * newColor.a) < alphaBlendOp >
			(dstAlphaBlendFactor * oldColor.a);
	}
	else {
		finalColor = newColor;

	}
	finalColor = finalColor & colorWriteMask;

透明混合伪代码:

	finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
	finalColor.a = newAlpha.a;

全局颜色混合设置:

	VkPipelineColorBlendStateCreateInfo colorBlending = {};
	colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
	//bitwise combination 自动禁用第一种方法
	colorBlending.logicOpEnable = VK_FALSE;
	colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional
	colorBlending.attachmentCount = 1;
	colorBlending.pAttachments = &colorBlendAttachment;
	colorBlending.blendConstants[0] = 0.0f; // Optional
	colorBlending.blendConstants[1] = 0.0f; // Optional
	colorBlending.blendConstants[2] = 0.0f; // Optional
	colorBlending.blendConstants[3] = 0.0f; // Optional

Dynamic state

   本节的所有结构体中指定的一些参数可以更改,而无需重新创建管线。 例如,视口的大小,线宽和混合常量。填写VkPipelineDynamicStateCreateInfo结构,如下所示:

	//详情后续补充
	VkDynamicState dynamicStates[] = {
		VK_DYNAMIC_STATE_VIEWPORT,
		VK_DYNAMIC_STATE_LINE_WIDTH
	};
	
	VkPipelineDynamicStateCreateInfo dynamicState = {};
	dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
	dynamicState.dynamicStateCount = 2;
	dynamicState.pDynamicStates = dynamicStates;

Pipeline Layout

   uniform的使用,在管线创建期间,需要通过创建VkPipelineLayout对象来指定。

创建一个空的 pipeline layout:

	//详情后续补充
	VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
	pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
	pipelineLayoutInfo.setLayoutCount = 0; // Optional
	pipelineLayoutInfo.pSetLayouts = nullptr; // Optional
	pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional

	pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional
	if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
		throw std::runtime_error("failed to create pipeline layout!");
	}

销毁:

	vkDestroyPipelineLayout(device, pipelineLayout, nullptr);

Render passes

  在完成创建管线之前,需确定渲染时使用的framebuffer attachments。 指定管线有多少个color buffers和depth buffers,每个缓冲区要使用多少个samples,以及在整个渲染过程中应如何处理它们。

Attachment description

	VkAttachmentDescription colorAttachment = {};
	//colorAttachment的format应与swapChain图像的格式匹配
	colorAttachment.format = swapChainImageFormat;
	//没有使用多重采样(multisampling),将使用1个样本。
	colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
	//clear the framebuffer to black before drawing a new frame
	//VK_ATTACHMENT_LOAD_OP_LOAD 保留Attachment的现有内容
	//VK_ATTACHMENT_LOAD_OP_CLEAR 开始时将值初始化为常数
	//VK_ATTACHMENT_LOAD_OP_DONT_CARE 现有内容未定义; 忽略
	colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
	//color and depth data
	//VK_ATTACHMENT_STORE_OP_STORE 渲染的内容将存储在内存中,以后可以读取
	//VK_ATTACHMENT_STORE_OP_DONT_CARE 渲染操作后,帧缓冲区的内容将是未定义的
	colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
	//stencil data
	colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
	colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
	//Textures and framebuffers 
	//指定在渲染开始之前图像将具有的布局。
	colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
	//指定在渲染完成时自动过渡到的布局。
	//VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL used as color attachment
	//VK_IMAGE_LAYOUT_PRESENT_SRC_KHR Images the swap chain 中要显示的图像
	//VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL  用作存储器复制操作目的图像
	colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

Subpasses and attachment references

  单个render pass可以包含多个subpasses,每个subpass都引用了上一节中的结构描述的一个或多个attachments 。

	//Vulkan may also support compute subpasses in the future
	VkSubpassDescription subpass = {};
	subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
	//引用colorAttachment
	//数组中attachment的索引是直接从fragment shader 引用的(location = 0)out vec4 outColor指令!
	//subpass可以引用以下其他类型的attachment
	//pInputAttachments read from a shader
	//pResolveAttachments used for multisampling attachments
	//pDepthStencilAttachment depth and stencil data
	//pPreserveAttachments not used by this subpass but for which the data must be preserved(保存)
	subpass.colorAttachmentCount = 1;
	subpass.pColorAttachments = &colorAttachmentRef;

Render pass

	VkRenderPassCreateInfo renderPassInfo = {};
	renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
	renderPassInfo.attachmentCount = 1;
	renderPassInfo.pAttachments = &colorAttachment;
	renderPassInfo.subpassCount = 1;
	renderPassInfo.pSubpasses = &subpass;

	if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS)
	{

		throw std::runtime_error("failed to create render pass!");
	}
	-------------------------
	vkDestroyRenderPass(device, renderPass, nullptr);

Create The Graphics Pipeline

我们现在以创建的对象类型:

  • Shader stages,定义图形管线可编程阶段功能的着色器模块
  • Fixed-function state,定义管线固定功能阶段的所有结构,例如input assembly,rasterizer,viewport和color
    blending
  • Pipeline layout,着色器引用的uniform和values referenced,可以在绘制时进行更新
  • Render pass,attachment的引用以及使用规则
	VkGraphicsPipelineCreateInfo pipelineInfo = {};
	pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
	pipelineInfo.stageCount = 2;
	pipelineInfo.pStages = shaderStages;

	pipelineInfo.pVertexInputState = &vertexInputInfo;
	pipelineInfo.pInputAssemblyState = &inputAssembly;
	pipelineInfo.pViewportState = &viewportState;
	pipelineInfo.pRasterizationState = &rasterizer;
	pipelineInfo.pMultisampleState = &multisampling;
	pipelineInfo.pDepthStencilState = nullptr; // Optional
	pipelineInfo.pColorBlendState = &colorBlending;
	pipelineInfo.pDynamicState = nullptr; // Optional

	pipelineInfo.layout = pipelineLayout;
	//引用将使用图形管线的renderPass和subpass索引。
	//也可以使用其他渲染管线,但是它们必须与renderPass兼容
	pipelineInfo.renderPass = renderPass;
	pipelineInfo.subpass = 0;
	//Vulkan允许通过从现有管线中派生来创建新的图形管线。 
	//管线派生的思想是,当管线具有与现有管线共有的许多功能时,建立管线的成本较低,
	//并且可以更快地完成同一父管线之间的切换。 
	//可以使用basePipelineHandle指定现有管线的句柄,也可以使用basePipelineIndex引用索引创建的另一个管线。
	//现在只有一个管线,因此我们只需指定一个空句柄和一个无效索引。
	//仅当在VkGraphicsPipelineCreateInfo的flags字段中还指定了VK_PIPELINE_CREATE_DERIVATIVE_BIT标志时,才使用这些值。
	pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional
	pipelineInfo.basePipelineIndex = -1; // Optional

	//第二个参数VK_NULL_HANDLE引用了一个可选的VkPipelineCache对象。
	//管线Cache可用于存储和重用管线创建相关的数据
	//可以加快管线的创建速度,后有详解
	if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) {
		 throw std::runtime_error("failed to create graphics pipeline!");
	}

Code

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#include <vulkan/vulkan.h>

#include <iostream>
#include <GLFW/glfw3.h>

#include <vulkan/vulkan.h>

#include <iostream>  //[1]
#include <stdexcept> //[1]异常处理函数
#include <functional>//[1]提供 EXIT_SUCCESS and EXIT_FAILURE 宏指令
#include <cstdlib>
#include <vector>
#include <map>
#include <optional>
#include <set>
#include <cstdint> // Necessary for UINT32_MAX
#include <fstream>
const int WIDTH = 800;
const int HEIGHT = 600;



//ate: Start reading at the end of the file
//binary: Read the file as binary file (avoid text transformations)
static std::vector<char> readFile(const std::string& filename) {
	std::ifstream file(filename, std::ios::ate | std::ios::binary);
	if (!file.is_open()) {
		throw std::runtime_error("failed to open file!");
	}
	//ate的优势是,可以获取文件的大小
	size_t fileSize = (size_t)file.tellg();
	std::vector<char> buffer(fileSize);
	//指针跳到头
	file.seekg(0);
	file.read(buffer.data(), fileSize);
	file.close();
	return buffer;
}

class HelloTriangleApplication
{

public:
	void run() 
	{
		initVulkan(); //[1]初始化Vulakn相关
		cleanup();    //[1]释放资源
	}
private:

	void initVulkan() 
	{
		//...
		//[10]
		createRenderPass();
		createGraphicsPipeline();
	}

	void cleanup() 
	{
		//[15]
		vkDestroyPipeline(device, graphicsPipeline, nullptr);
		//[9]
		vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
		//[10]
		vkDestroyRenderPass(device, renderPass, nullptr);
		//...
	}

	void createGraphicsPipeline() {

		//[1]
		auto vertShaderCode = readFile("shaders/vert.spv");
		auto fragShaderCode = readFile("shaders/frag.spv");
		//[1]
		VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
		VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);

		//[1]可以将多个着色器组合到一个ShaderModule中,并使用不同的entry points来区分它们的行为。
		VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
		vertShaderStageInfo.sType =	VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
		vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
		vertShaderStageInfo.module = vertShaderModule;
		vertShaderStageInfo.pName = "main";
		//[1]指定着色器常量的值。 
		//[1]单个着色器模块,在管道中创建常量,给予不同的值来配置其行为。
		//[1]比在渲染时使用变量配置着色器更为有效, 编译器可以进行优化,例如消除依赖于这些值的if语句
		//[1]如果没有这样的常量,则可以将成员设置为nullptr,初始化会自动执行该操作。
		//[1]pSpecializationInfo	
		
		//[1]
		VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
		fragShaderStageInfo.sType =	VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
		fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
		fragShaderStageInfo.module = fragShaderModule;
		fragShaderStageInfo.pName = "main";
		//[1]
		VkPipelineShaderStageCreateInfo shaderStages[] = { vertShaderStageInfo, fragShaderStageInfo };

		//[2]
		VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
		vertexInputInfo.sType =	VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
		vertexInputInfo.vertexBindingDescriptionCount = 0;
		//[2]描述上述用于加载顶点数据的详细信息
		vertexInputInfo.pVertexBindingDescriptions = nullptr; 
		vertexInputInfo.vertexAttributeDescriptionCount = 0;
		//[2]描述上述用于加载顶点数据的详细信息
		vertexInputInfo.pVertexAttributeDescriptions = nullptr; 

		//[3]
		VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
		inputAssembly.sType =	VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
		inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
		inputAssembly.primitiveRestartEnable = VK_FALSE;

		//[4]viewport
		VkViewport viewport = {};
		viewport.x = 0.0f;
		viewport.y = 0.0f;
		viewport.width = (float)swapChainExtent.width;
		viewport.height = (float)swapChainExtent.height;
		//[3]minDepth和maxDepth 在0.0f到1.0f之间
		viewport.minDepth = 0.0f;
		viewport.maxDepth = 1.0f;

		//[3]裁剪矩形定义哪些区域像素被存储
		VkRect2D scissor = {};
		scissor.offset = { 0, 0 };
		scissor.extent = swapChainExtent;

		//[3]viewport 和scissor可以有多个
		VkPipelineViewportStateCreateInfo viewportState = {};
		viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
		viewportState.viewportCount = 1;
		viewportState.pViewports = &viewport;
		viewportState.scissorCount = 1;
		viewportState.pScissors = &scissor;

		VkPipelineRasterizationStateCreateInfo rasterizer = {};
		rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
		//[4]保留近平面和远平面之外的片元
		rasterizer.depthClampEnable = VK_FALSE;
		//[4]true 几何图形不会通过光栅化阶段。 禁用对帧缓冲区的任何输出
		rasterizer.rasterizerDiscardEnable = VK_FALSE;
		//[4]VK_POLYGON_MODE_FILL 用片段填充多边形区域
		//[4]VK_POLYGON_MODE_LINE 多边形边缘绘制为线
		//[4]VK_POLYGON_MODE_POINT 多边形顶点绘制为点
		rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
		//[4]支持的最大线宽取决于硬件,任何比1.0f粗的线都需要启用wideLines。
		rasterizer.lineWidth = 1.0f;
		//[4]确定要使用的面部剔除类型。
		rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
		//[4]指定面片视为正面的顶点顺序,可以是顺时针或逆时针
		rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
		//[4]Rasterization可以通过添加常量或基于片段的斜率对深度值进行偏置来更改深度值。
		//[4]多用于阴影贴图,如不需要将depthBiasEnable设置为VK_FALSE。
		rasterizer.depthBiasEnable = VK_FALSE;
		rasterizer.depthBiasConstantFactor = 0.0f; 
		rasterizer.depthBiasClamp = 0.0f; 
		rasterizer.depthBiasSlopeFactor = 0.0f; 

		//[5]
		VkPipelineMultisampleStateCreateInfo multisampling = {};
		multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
		multisampling.sampleShadingEnable = VK_FALSE;
		multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
		multisampling.minSampleShading = 1.0f; // Optional
		multisampling.pSampleMask = nullptr; // Optional
		multisampling.alphaToCoverageEnable = VK_FALSE; // Optional
		multisampling.alphaToOneEnable = VK_FALSE; // Optional

		//[6]specification详见说明文档
		//[6]每个附加的帧缓冲区的混合规则 
		VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
		colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT |
			VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT |
			VK_COLOR_COMPONENT_A_BIT;
		//[6]片段着色器的颜色直接输出
		colorBlendAttachment.blendEnable = VK_FALSE;
		colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; 
		colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; 
		colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; 
		colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; 
		colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; 
		colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; 


		/*
			if (blendEnable) {
				finalColor.rgb = (srcColorBlendFactor * newColor.rgb)
					< colorBlendOp > (dstColorBlendFactor * oldColor.rgb);
				finalColor.a = (srcAlphaBlendFactor * newColor.a) < alphaBlendOp >
					(dstAlphaBlendFactor * oldColor.a);
			}
			else {
				finalColor = newColor;

			}
			finalColor = finalColor & colorWriteMask;
		*/
		//[6]透明混合
		/*	
			finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;
			finalColor.a = newAlpha.a;
		*/
		//[6]VK_TRUE
		colorBlendAttachment.blendEnable = VK_TRUE;
		colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
		colorBlendAttachment.dstColorBlendFactor =	VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
		colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
		colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
		colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
		colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;

		//[6]全局颜色混合设置。
		VkPipelineColorBlendStateCreateInfo colorBlending = {};
		colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
		//[6]bitwise combination 请注意,这将自动禁用第一种方法
		colorBlending.logicOpEnable = VK_FALSE;
		colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional
		colorBlending.attachmentCount = 1;
		colorBlending.pAttachments = &colorBlendAttachment;
		colorBlending.blendConstants[0] = 0.0f; // Optional
		colorBlending.blendConstants[1] = 0.0f; // Optional
		colorBlending.blendConstants[2] = 0.0f; // Optional
		colorBlending.blendConstants[3] = 0.0f; // Optional

		//[7]
		VkDynamicState dynamicStates[] = {
			 VK_DYNAMIC_STATE_VIEWPORT,
			 VK_DYNAMIC_STATE_LINE_WIDTH
		};

		//[7]
		VkPipelineDynamicStateCreateInfo dynamicState = {};
		dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
		dynamicState.dynamicStateCount = 2;
		dynamicState.pDynamicStates = dynamicStates;
		//[8]
		VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
		pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
		pipelineLayoutInfo.setLayoutCount = 0; // Optional
		pipelineLayoutInfo.pSetLayouts = nullptr; // Optional
		pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional
		pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional

		//[8]
		if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
			throw std::runtime_error("failed to create pipeline layout!");
		}


		//[13]
		VkGraphicsPipelineCreateInfo pipelineInfo = {};
		pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
		pipelineInfo.stageCount = 2;
		pipelineInfo.pStages = shaderStages;

		pipelineInfo.pVertexInputState = &vertexInputInfo;
		pipelineInfo.pInputAssemblyState = &inputAssembly;
		pipelineInfo.pViewportState = &viewportState;
		pipelineInfo.pRasterizationState = &rasterizer;
		pipelineInfo.pMultisampleState = &multisampling;
		pipelineInfo.pDepthStencilState = nullptr; // Optional
		pipelineInfo.pColorBlendState = &colorBlending;
		pipelineInfo.pDynamicState = nullptr; // Optional

		pipelineInfo.layout = pipelineLayout;
		//[13]引用将使用图形管线的renderPass和subpass索引。
		//[13]也可以使用其他渲染管线,但是它们必须与renderPass兼容
		pipelineInfo.renderPass = renderPass;
		pipelineInfo.subpass = 0;
		//[13]Vulkan允许通过从现有管线中派生来创建新的图形管线。 
		//[13]管线派生的思想是,当管线具有与现有管线共有的许多功能时,建立管线的成本较低,
		//[13]并且可以更快地完成同一父管线之间的切换。 
		//[13]可以使用basePipelineHandle指定现有管线的句柄,也可以使用basePipelineIndex引用索引创建的另一个管线。
		//[13]现在只有一个管线,因此我们只需指定一个空句柄和一个无效索引。
		//[13]仅当在VkGraphicsPipelineCreateInfo的flags字段中还指定了VK_PIPELINE_CREATE_DERIVATIVE_BIT标志时,才使用这些值。
		pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional
		pipelineInfo.basePipelineIndex = -1; // Optional


		//[14]第二个参数VK_NULL_HANDLE引用了一个可选的VkPipelineCache对象。
		//[14]管线Cache可用于存储和重用管线创建相关的数据
		//[14]可以加快管线的创建速度,后有详解
		if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) {
			 throw std::runtime_error("failed to create graphics pipeline!");
		}


		//[1]
		//在创建图形管线之前,不会将SPIR-V字节码编译并链接到机器代码以供GPU执行。
		//这意味着在管道创建完成后,就可以立即销毁ShaderModule
		vkDestroyShaderModule(device, fragShaderModule, nullptr);
		vkDestroyShaderModule(device, vertShaderModule, nullptr);
	}
	//[1]
	VkShaderModule createShaderModule(const std::vector<char>&code) {
		VkShaderModuleCreateInfo createInfo = {};
		createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
		createInfo.codeSize = code.size();
		createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());
		VkShaderModule shaderModule;
		if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
			throw std::runtime_error("failed to create shader module!");
		}
		return shaderModule;
	}
	void createRenderPass() {
		VkAttachmentDescription colorAttachment = {};
		//[10]colorAttachment的format应与swapChain图像的格式匹配
		colorAttachment.format = swapChainImageFormat;
		//[10]没有使用多重采样(multisampling),将使用1个样本。
		colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
		//[10]clear the framebuffer to black before drawing a new frame
		//[10]VK_ATTACHMENT_LOAD_OP_LOAD 保留Attachment的现有内容
		//[10]VK_ATTACHMENT_LOAD_OP_CLEAR 开始时将值初始化为常数
		//[10]VK_ATTACHMENT_LOAD_OP_DONT_CARE 现有内容未定义; 忽略
		colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
		//[10]color and depth data
		//[10]VK_ATTACHMENT_STORE_OP_STORE 渲染的内容将存储在内存中,以后可以读取
		//[10]VK_ATTACHMENT_STORE_OP_DONT_CARE 渲染操作后,帧缓冲区的内容将是未定义的
		colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
		//[10]stencil data
		colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
		colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
		//[10]Textures and framebuffers 
		//[10]指定在渲染开始之前图像将具有的布局。
		colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
		//[10]指定在渲染完成时自动过渡到的布局。
		//[10]VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL used as color attachment
		//[10]VK_IMAGE_LAYOUT_PRESENT_SRC_KHR Images the swap chain 中要显示的图像
		//[10]VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL  用作存储器复制操作目的图像
		colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

		//[11]引用Attachment
		VkAttachmentReference colorAttachmentRef = {};
		//[11]attachment参数通过attachment描述数组中的索引指定要引用的attachment
		colorAttachmentRef.attachment = 0;
		//[11]布局指定了我们希望attachment在使用此引用的subpass中具有哪种布局。
		colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

		//[11]Vulkan may also support compute subpasses in the future
		VkSubpassDescription subpass = {};
		subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
		//[11]引用colorAttachment
		//[11]数组中attachment的索引是直接从fragment shader 引用的(location = 0)out vec4 outColor指令!
		//[11]subpass可以引用以下其他类型的attachment
		//[11]pInputAttachments read from a shader
		//[11]pResolveAttachments used for multisampling attachments
		//[11]pDepthStencilAttachment depth and stencil data
		//[11]pPreserveAttachments not used by this subpass but for which the data must be preserved(保存)
		subpass.colorAttachmentCount = 1;
		subpass.pColorAttachments = &colorAttachmentRef;

		//[12]
		VkRenderPassCreateInfo renderPassInfo = {};
		renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
		renderPassInfo.attachmentCount = 1;
		renderPassInfo.pAttachments = &colorAttachment;
		renderPassInfo.subpassCount = 1;
		renderPassInfo.pSubpasses = &subpass;

		if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS)
		{

			throw std::runtime_error("failed to create render pass!");
		}


	}

private:
	VkExtent2D swapChainExtent;
	VkSwapchainKHR swapChain;
	VkDevice device;
	VkPipelineLayout pipelineLayout;
	VkRenderPass renderPass;
	VkPipeline graphicsPipeline;
};


int main() {
	HelloTriangleApplication app;
	//[1]捕获异常
	try {
		app.run();
	}
	catch (const std::exception& e) {
		std::cerr << e.what() << std::endl;
		return EXIT_FAILURE;
	}

	return EXIT_SUCCESS;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vulkan被称为OpenGL的接班人,性能果然是霸气外漏,更能够承载下一个时代的图形渲染编程。 GPU高性能渲染的课题进入了一个新的阶段,对于计算细节的控制,多核CPU多线程渲染以及高性能算法的灵活设计需求日益旺盛。图形程序员需要有更加强力且灵活的工具,来“解锁”我们自身的控制能力,OpenGL的较高度封装性以及单纯的状态机模式显然已经无法适应现代化图形渲染的强烈需求。为什么要学习Vulkan?正如前言所说,Vulkan已经成为了下一个时代的图形渲染主流API,早已经被各大商业引擎(Unreal Engine、Unity3D)所支持。那么我们的同学就有如下问题需要明晰:1 作为游戏程序员我们只学会了UE或者Unity3D,那么就只能作为一个普通的程序员,如果能够结合Vulkan学习对商用引擎理解更加深刻,就可以更好的发挥引擎威力甚至更改引擎的源代码,实现更多的可能,让你在各大公司之间都“蛇形走位,游刃有余” 2 作为自研引擎工作人员,你可能在工业软件领域从业、也有可能在影视渲染领域从业、也可能在其他的图形系统领域(军工、GIS、BIM)等领域,那么熟练的掌握Vulkan就可以针对自己公司的不同领域需求进行不同的引擎定制开发,从而获得牢不可破的地位,对于自身职业发展有着极大的优势! 总而言之,越早学习Vulkan,就越能与别人拉开差距,让Vulkan称为你升职加薪、壮大不可替代性的核武器! 课程简介:本课程详细讲解了Vulkan从小白到入门的基础理论+实践知识,对于每一个知识点都会带领学员通过代码来实现功能。其中涵盖了计算机图形学基础理论,计算机图形学数学推导,Vulkan基础系统设计理论,基础单元(实例,设备,交换链), 渲染管线,RenderPass, 指令与多线程, 顶点描述与实验, Uniform与描述符, 图像与采样, 深度与反走样,模型与摄像机等内容;课程中会对Vulkan复杂抽象的API进行一次包装层的封装,将相关的API都进行聚合与接口设计,作为游戏或者图形引擎来讲,这是至关重要的第一步。这一个封装步骤,也被称为API-Wrapper,经过包装后的类,同学可以在此之上根据自己的具体需求进行扩展,从而得到最适合自己的类内容。本课程为系列化课程,在铸造基石篇章之后,会继续使用本包装类进行改良,并且实现Vulkan API下的各类效果以及高级特性的开发教学。 课程优势:1 本课程会从计算机图形学的基础渲染管线原理出发,带领0基础的同学对计算机图形学进行快速认知,且对必要的知识点进行筛选提炼,去掉冗余繁杂的教学内容,更加适合新手对Vulkan渲染体系入门了解。 2 本课程会对计算机图形学所涉及的数学知识及如何应用到渲染当中,进行深入的讲解,带领同学对每一行公式展开认识,从三维世界如何映射到二维的屏幕,在学习完毕后会有清晰的知识体系 3 本课程会带领同学认知每一个Vulkan的API,并且在代码当中插入详细的注释,同学们在学习的时候就可以参照源代码进行一系列尝试以及学后复习 4 本课程所设计的包装层,会带领同学一行一行代码实现,现场进行Debug,对于Vulkan常出现的一些问题进行深入探讨与现场纠正  学习所得:1 同学们在学习后可以完全了解从三维世界的抽象物体,如何一步步渲染称为一个屏幕上的像素点。2 同学们在学习后可以完全掌握基础的Vulkan图形API,并且了解Vulkan当中繁多的对象之间相互的联系,从而可以设计更好的图形程序3 同学们在跟随课程进行代码编写后,可以获得一个轻量级的Vulkan底层API封装(Wrapper),从而可以在此之上封装上层的应用,得到自己的迷你Vulkan图形渲染引擎当然,在达到如上三点之后,如果可以更进一步学习Vulkan的进阶课程,同学们可以获得更好的职业发展,升职加薪之路会更加清晰,成为公司不可替代的强力工程师 本课程含有全套源代码,同学购买后,可以在课程附件当中下载 完全不懂图形学可以学习么?使用层面上来讲是没有问题的,老师在每个api讲解的时候,都会仔细分析api背后的原理,所以可以跟随下来的话,能够编程与原理相融,学会使用 数学不好可以学习么?学习图形类课程,最好能够入门级别的线性代数,具体说就是: 1 向量操作 2 矩阵乘法 3 矩阵的逆、转置 这几个点就足够 学习后对就业面试有什么作用?目前类似Vulkan的渲染知识是一切引擎的基础,只要能够跟随每一节课写代码做下来,游戏公司、工业软件公司等都是非常容易进去的,因为原理层面已经通晓,面试就会特别有优势。同学可以在简历上写熟悉VulkanAPI并且有代码经验,对于建立筛选以及面试都会有很大的帮助,对于薪资也会有大幅度提升

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值