Vulkan开发实战详解 学习笔记 - 背面剔除,间接绘制

首先对生成正方形面绘制用数据的ColorRect类进行修改,下面先给出修改后的类声明,具体内容如下

ColorRect.h

#ifndef VULKANEXBASE_COLORRECT_H
#define VULKANEXBASE_COLORRECT_H //防止重复定义
class ColorRect {
public:
	static float* vdataG; //青色正方形顶点数据指针
	static float* vdataY; //黄色正方形顶点数据指针
	static int dataByteCount; //每个正方形顶点数据所占总字节数
	static int vCount; //每个正方形顶点数量
	static float UNIT_SIZEG; //青色正方形边长
	static float UNIT_SIZEY; //黄色正方形边长
	static void genVertexData(); //生成顶点数据的方法
};
#endif

ColorRect.cpp

#include "ColorRect.h"
#include <vector>
#include <math.h>
#include <string.h>

float ColorRect::UNIT_SIZEG = 500; //青色正方形半边长
float ColorRect::UNIT_SIZEY = 499.5; //黄色正方形半边长
float* ColorRect::vdataG; //青色正方形顶点数据数组首地址指针
float* ColorRect::vdataY; //黄色正方形顶点数据数组首地址指针
int ColorRect::dataByteCount; //每个正方形顶点数据所占总字节数
int ColorRect::vCount; //每个正方形顶点数量
void ColorRect::genVertexData()
{ //生成顶点数据的方法
    vCount=6; //顶点数量
	dataByteCount = vCount * 6 * sizeof(float); //每个正方形顶点数据所占总字节数
	vdataG = new float[vCount * 6]
    { //青色正方形顶点数据数组
		0,0,0, 0,1,1, //第1 个点的位置和颜色数据
		UNIT_SIZEG,UNIT_SIZEG,0, 0,1,1, //第2 个点的位置和颜色数据
		-UNIT_SIZEG,UNIT_SIZEG,0, 0,1,1, //第3 个点的位置和颜色数据
		-UNIT_SIZEG,-UNIT_SIZEG,0, 0,1,1, //第4 个点的位置和颜色数据
		UNIT_SIZEG,-UNIT_SIZEG,0, 0,1,1, //第5 个点的位置和颜色数据
		UNIT_SIZEG,UNIT_SIZEG,0, 0,1,1 //第6 个点的位置和颜色数据
	};
	vdataY = new float[vCount * 6]
    {
		0,0,0,                        1,1,0,
		UNIT_SIZEY,UNIT_SIZEY,0,        1,1,0,
		-UNIT_SIZEY,UNIT_SIZEY,0,       1,1,0,
		-UNIT_SIZEY,-UNIT_SIZEY,0,      1,1,0,
		UNIT_SIZEY,-UNIT_SIZEY,0,       1,1,0,
		UNIT_SIZEY,UNIT_SIZEY,0,        1,1,0
	};//粉红色顶点坐标数据数组
}

上述代码的主要功能为生成青色、黄色两种颜色正方形的顶点数据,下面将要介绍的是设置两套不同投影参数的相关代码

void MyVulkanManager::initMatrix() 
{ //初始化矩阵
	MatrixState3D::setInitStack();//初始化基本变换矩阵
	float ratio = (float)screenWidth / (float)screenHeight;//求屏幕长宽比
	MatrixState3D::setCamera(5000.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f); //初始化摄像机
	float NEARt; //表示近平面参数的变量
	if (ProjectPara) { NEARt =800.0f; } //较大的NEAR 值
	else { NEARt = 1.0f; } //较小的NEAR 值
	MatrixState3D::setProjectFrustum(-NEARt*ratio*0.25f, NEARt*ratio*0.25f, //设置透视投影参数
		-NEARt*0.25f, NEARt*0.25f, NEARt, 10000.0f);
}

过点击屏幕改变ProjectPara变量的值,从而设置不同的透视参数,只是near值不同而已。

在这里插入图片描述

运行效果图中可以看出,虽然画面内容是相同的,,但当near值较小时(案例中取值为1)画面中物体的相互遮挡在某些位置出现了错误(应该被遮挡物体的某些位置没有被遮挡),而near值较大(案例中取值为800)时则基本没有问题。

这是由于物体的相互遮挡需要通过深度值来进行判断, ,而从near到far范围内的物体离摄像机的距离被映射到范围在−1~+1之间的深度值时采用的映射并不是线性的,:在视角不变、far参数相同的情况下,near值越小,距离near越近的位置占用的深度值范围越大,距离near值远一些的位置占用的深度值范围越小;near值越大,不同距离位置对应深度值的分布将越均匀。。这种情况下,由于远处的不同距离占用的深度值区间很小,故不同距离对应的深度值差异本身很小,再加上深度值一般用8或16bit表示,本身精度不高,就很容易造成距离很近的深度值最后的实际值相同,不能区分,进而造成依赖深度值的遮挡计算失败,产生不正确的画面。

综上所述,在能够保证期望观察到的物体都在视景体中的情况下,可以将near值尽量增大,far值尽量减小,使得远处深度值的计算更加精确,避免出现深度检测冲突而导致画面遮挡错误的出现。然而,near值也不是越大越好,far值也不是越小越好,这是因为在摄像机到近平面之间的物体是不可见的,远平面之外的物体也是不可见的。

深度偏移

实际开发过程中,往往会出现需要绘制的两个不同面重叠的情况,这时很可能会出现错误的效果,如图4-42所示。这些错误的效果是因为光栅化的精度有限而产生的。为了避免错误效果的产生,可以采用Vulkan的深度偏移技术使重叠面看起来好像不共面。

所谓深度偏移,其基本原理是通过给重叠的面增加深度偏移值,使重叠面看起来并不共面,以便重叠面能够被正确渲染。这种技术是很有用的,例如要渲染投射在墙上的阴影,这时候墙和阴影共面。如果没有深度偏移,先渲染墙,再渲染阴影,经深度测试,阴影可能不能正确显示。假如给墙设置一个深度偏移值,使其深度值适当增大,然后渲染墙,再渲染阴影,则墙和阴影可以正确地显示。

修改数据

ColorRect.h

#ifndef VULKANEXBASE_COLORRECT_H
#define VULKANEXBASE_COLORRECT_H //防止重复定义
#include <cstdint>

class ColorRect
{
public:
	static float* vdataG; //青色正方形顶点数据指针
	static float* vdataR;//粉红色正方形顶点数据指针
	static int dataByteCount; //每个正方形顶点数据所占总字节数
	static int vCount; //每个正方形顶点数量
	static void genVertexData(); //生成顶点数据的方法
};
#endif

ColorRect.cpp

#include "ColorRect.h"

#define UINT_SIZE 600
float* ColorRect::vdataG; //青色正方形顶点数据数组首地址指针
float* ColorRect::vdataR;
int ColorRect::dataByteCount; //每个正方形顶点数据所占总字节数
int ColorRect::vCount; //每个正方形顶点数量
void  ColorRect::genVertexData()
{
    vCount=6; //顶点数量
	dataByteCount = vCount * 6 * sizeof(float); //每个正方形顶点数据所占总字节数
	vdataG = new float[vCount * 6]{ //青色正方形顶点数据数组
		0,0,0, 0,1,1, //第1 个点的位置和颜色数据
		UINT_SIZE,UINT_SIZE,0,        0,1,1,
		-UINT_SIZE,UINT_SIZE,0,       0,1,1,
		-UINT_SIZE,-UINT_SIZE,0,      0,1,1,
		UINT_SIZE,-UINT_SIZE,0,       0,1,1,
		UINT_SIZE,UINT_SIZE,0,        0,1,1
	};
	vdataR = new float[vCount * 6]{
		0,0,0,                        1,1,0,
		UINT_SIZE,UINT_SIZE,0,        1,1,0,
		-UINT_SIZE,UINT_SIZE,0,       1,1,0,
		-UINT_SIZE,-UINT_SIZE,0,      1,1,0,
		UINT_SIZE,-UINT_SIZE,0,       1,1,0,
		UINT_SIZE,UINT_SIZE,0,        1,1,0
	};//粉红色顶点坐标数据数组

}

深度偏移的一些参数

	rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;	//卷绕方向为逆时针
	rs.depthClampEnable = VK_TRUE;//深度截取 
	rs.rasterizerDiscardEnable = VK_FALSE;//启用光栅化操作(若为TRUE则光栅化不产生任何片元)
	rs.depthBiasEnable = VK_TRUE;//启用深度偏移
	rs.depthBiasConstantFactor = 0;	//深度偏移常量因子
	rs.depthBiasClamp = 0;//深度偏移值上下限(若为正作为上限,为负作为下限)
	rs.depthBiasSlopeFactor = 0;//深度偏移斜率因子
	rs.lineWidth = 1.0f;

上述代码的主要功能为启用深度偏移,并设置用于计算深度偏移的3个参数值。depthBiasEnable、depthBiasConstantFactor、depthBiasClamp、depthBiasSlopeFactor这4个属性都来自于管线光栅化状态创建信息结构体VkPipelineRasterizationStateCreateInfo。

是管线封装类ShaderQueueSuit_Common,首先需要启用深度偏移动态设置

void ShaderQueueSuit_Common::create_pipe_line(VkDevice& device, VkRenderPass& renderPass)
{	
    #define NUM_DYNAMIC_STATES 1 /*Viewport + Scissor*/	                               
    //VkDynamicState dynamicStateEnables[VK_DYNAMIC_STATE_RANGE_SIZE];//动态状态启用标志
    VkDynamicState dynamicStateEnables[NUM_DYNAMIC_STATES];//动态状态启用标志
    memset(dynamicStateEnables, 0, sizeof dynamicStateEnables);	//设置所有标志为false
    dynamicStateEnables[0] = VK_DYNAMIC_STATE_VIEWPORT;		        //视口为动态设置

    VkPipelineDynamicStateCreateInfo dynamicState = {};//管线动态状态创建信息
    dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;//结构体类型
    dynamicState.pNext = NULL;//自定义数据的指针
    dynamicState.pDynamicStates = dynamicStateEnables;//动态状态启用标志数组
    dynamicState.dynamicStateCount = 0;//启用的动态状态项数量

是将深度偏移动态设置标志(VK_DYNAMIC_STATE_DEPTH_BIAS)放入管线动态状态启用标志数组,并相应改变了管线动态状态创建信息结构体实例中启用的动态状态项数量。

//绘制方法

		MatrixState3D::rotate(xAngle, 1, 0, 0);								//绕X轴旋转xAngle
		MatrixState3D::rotate(yAngle, 0, 1, 0);								//绕Y轴旋转yAngle
		//行首先设置青色矩形对应的深度偏移参数,然后结合平移变换执行绘制
		vkCmdSetDepthBias(cmdBuffer, 0.0, 0.0, 0.0); //设置青色矩形的深度偏移信息

		MatrixState3D::pushMatrix();									//保护现场
		MatrixState3D::translate(-250.0f, 0.0f, 0.0f); //沿X轴负方向平移250
		
		colorRectG->drawSelf(cmdBuffer, //绘制青色矩形
			sqsCL->pipelineLayout, sqsCL->pipeline, &(sqsCL->descSet[0]));
		MatrixState3D::popMatrix();									//恢复现场

		switch (depthOffsetFlag) { //根据索引设置黄色矩形深度偏移参数
		case 0:break;
        //vkCmdSetDepthBias方法共有4个入口参数,依次为使用的命令缓冲、深度偏移常量因子、深度偏移值上下限、深度偏移斜率因子。
		case 1: vkCmdSetDepthBias(cmdBuffer, -1.0, -3.0, -2.0); break; //黄色矩形深度值减小
		case 2: vkCmdSetDepthBias(cmdBuffer, 1.0, 3.0, 2.0); break; //黄色矩形深度值增大        
		} 
		//先根据depthOffsetFlag值的不同设置不同的深度偏移参数,然后结合平移变换绘制黄色矩形。
		MatrixState3D::pushMatrix();									//保护现场
		MatrixState3D::translate(250.0f, 0.0f, 0.0f); //沿X 轴正方向平移250
		colorRectR->drawSelf(cmdBuffer, //绘制黄色矩形
			sqsCL->pipelineLayout, sqsCL->pipeline, &(sqsCL->descSet[0]));

在这里插入图片描述

卷绕和背面剪裁

若不打开背面剪裁,管线会对所有的面都进行绘制。被遮挡面上的片元虽然被绘制了,但还是会被遮挡面上的片元所覆盖,最终并不会出现在屏幕上。这导致很多绘制工作都白做了,宝贵的计算资源被浪费,同样情况下应用程序的帧速率(FPS)可能会下降很多。

对于封闭立体物体的渲染,一般情况下应该打开背面剪裁。但如果绘制的是平面物体,希望在正面和背面观察都能看到就不应该打开背面剪裁了。实际开发中,读者应该根据具体需要来选择。

void ShaderQueueSuit_Common::create_pipe_line(VkDevice& device, VkRenderPass& renderPass)
{
	rs.cullMode = VK_CULL_MODE_NONE;//不使用背面剪裁
	rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;	//卷绕方向为逆时针
}

VK_CULL_MODE_FRONT_BIT //剪裁正面

VK_CULL_MODE_BACK_BIT //剪裁背面

VK_CULL_MODE_NONE;//不使用背面剪裁

VK_FRONT_FACE_COUNTER_CLOCKWISE表示以逆时针方向为正面,顺时针方向为背面。
VK_FRONT_FACE_CLOCKWISE表示以顺时针方向为正面,逆时针方向为背面

TriangleData.cpp

#include "TriangleData.h"
#include <vector>
#include <math.h>
#include <string.h>
//的主要功能为生成两个卷绕方向相反的三角形顶点数据
float* TriangleData::vdata;//数据数组首地址指针
int TriangleData::dataByteCount;//数据所占总字节数量
int TriangleData::vCount;//顶点数量
void  TriangleData::genVertexData() {//顶点数据生成方法
	vCount = 6; //顶点数量
	dataByteCount = vCount * 6 * sizeof(float);//数据所占内存总字节数
	vdata = new float[vCount * 6]{ //数据数组
		-90,60,0, 1,1,1, //左侧三角形第一个顶点数据
		-90,-60,0, 0,1,0, //左侧三角形第二个顶点数据
		-30,-60,0, 1,1,1, //左侧三角形第三个顶点数据
		30,60,0, 1,1,1, //右侧三角形第一个顶点数据
		90,60,0, 1,1,1, //右侧三角形第二个顶点数据
		90,-60,0, 0,1,0, //右侧三角形第三个顶点数据
	};
}

VK_CULL_MODE_NONE;//不使用背面剪裁

rs.polygonMode = VK_POLYGON_MODE_FILL;//绘制方式为填充
rs.cullMode = VK_CULL_MODE_NONE;//不使用背面剪裁
rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;

在这里插入图片描述

rs.cullMode = VK_CULL_MODE_FRONT_BIT;  //剪裁正面
rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
rs.cullMode = VK_CULL_MODE_BACK_BIT;  //剪裁背面
rs.frontFace = VK_FRONT_FACE_CLOCKWISE;	

在这里插入图片描述

	rs.polygonMode = VK_POLYGON_MODE_FILL;//绘制方式为填充
	rs.cullMode = VK_CULL_MODE_BACK_BIT;  剪裁背面
	rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
rs.polygonMode = VK_POLYGON_MODE_FILL;//绘制方式为填充
rs.cullMode = VK_CULL_MODE_FRONT_BIT;  //剪裁正面
rs.frontFace = VK_FRONT_FACE_CLOCKWISE;	//卷绕方向为逆时针

在这里插入图片描述

间接绘制

都是通过调用vkCmdDraw或vkCmdDrawIndexed方法来实现物体的绘制。通过这两个方法实现物体绘制时,需要在调用绘制方法时提供顶点数量、实例数量、索引数量等实际绘制执行时所需的信息,这种模式可以称之为直接绘制。

但有些情况下,CPU调用绘制方法时还不能确定实际绘制执行时所需的信息,此时就无法使用直接绘制了。这种情况下可以采用间接绘制。

请读者注意CPU调用vkCmdDraw或vkCmdDrawIndexed方法时只是将方法记录到命令缓冲中,以备GPU真正实施绘制时执行。故CPU调用vkCmdDraw或vkCmdDrawIndexed方法时并没有真正实施绘制,一直到记录了绘制工作的命令缓冲被提交到GPU中的队列执行时才真正实施绘制。因而,绘制命令被记录到命令缓冲与绘制实际执行之间是有时间差的。

class DrawableObjectCommonLight
{
public:
	int indirectDrawCount; //间接绘制信息数据组的数量
	int drawCmdbufbytes; //间接绘制信息数据所占总字节数
	VkBuffer drawCmdbuf; //间接绘制信息数据缓冲
	VkDeviceMemory drawCmdMem; //间接绘制信息数据缓冲对应设备内存
	void initDrawCmdbuf(VkDevice& device, VkPhysicalDeviceMemoryProperties& memoryroperties); //用于创建间接绘制信息数据缓冲的方法

}

主要就是在绘制用物体类的头文件中增加了与间接绘制信息数据缓冲相关的成员变量声明以及方法声明

void DrawableObjectCommonLight::initDrawCmdbuf( //用于创建间接绘制信息数据缓冲的方法
	VkDevice& device, VkPhysicalDeviceMemoryProperties& memoryroperties) {
	//行首先给出了间接绘制信息数据组的数量,本案例中此数量为1。
    indirectDrawCount = 1; //间接绘制信息数据组的数量
    //然后根据数量与VkDrawIndirectCommand类型所占字节数计算出了间接绘制信息数据所占总字节数。
	drawCmdbufbytes = indirectDrawCount * sizeof(VkDrawIndirectCommand); //信息数据所占总字节数
	//行设置缓冲的用途为间接绘制信息数据缓冲。
    VkBufferCreateInfo buf_info = {}; //构建缓冲创建信息结构体实例
	buf_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; //设置结构体类型
	buf_info.pNext = NULL; //自定义数据的指针
	buf_info.usage = VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; //设置缓冲用途
	buf_info.size = drawCmdbufbytes; //设置数据总字节数
	buf_info.queueFamilyIndexCount = 0; //队列家族数量
	buf_info.pQueueFamilyIndices = NULL; //队列家族列表
	buf_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; //共享模式
	buf_info.flags = 0; //标志
	VkResult result = vkCreateBuffer(device, &buf_info, NULL, &drawCmdbuf); //创建缓冲
	assert(result == VK_SUCCESS); //检查创建缓冲是否成功
	VkMemoryRequirements mem_reqs; //缓冲内存需求
	vkGetBufferMemoryRequirements(device, drawCmdbuf, &mem_reqs); //获取缓冲内存需求
	assert(drawCmdbufbytes <= mem_reqs.size); //检查内存需求获取是否正确
	VkMemoryAllocateInfo alloc_info = {}; //构建内存分配信息结构体实例
	alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;//设置结构体类型
	alloc_info.pNext = NULL; //自定义数据的指针
	alloc_info.memoryTypeIndex = 0; //内存类型索引
	alloc_info.allocationSize = mem_reqs.size; //内存总字节数
	VkFlags requirements_mask = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT
		| VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; //需要的内存类型掩码
	bool flag = memoryTypeFromProperties(memoryroperties, //获取所需内存类型索引
		mem_reqs.memoryTypeBits, requirements_mask, &alloc_info.memoryTypeIndex);
	if (flag) {
		printf("确定内存类型成功 类型索引为%d", alloc_info.memoryTypeIndex);
	}
	else {
		printf("确定内存类型失败!");
	}
	result = vkAllocateMemory(device, &alloc_info, NULL, &drawCmdMem);//为缓冲分配内存
	assert(result == VK_SUCCESS); //检查内存分配是否成功
	uint8_t *pData; // CPU 访问时的辅助指针
	result = vkMapMemory(device, //将设备内存映射为CPU 可访问
		drawCmdMem, 0, mem_reqs.size, 0, (void **)&pData);
	assert(result == VK_SUCCESS); //检查映射是否成功
	 //构建了一个间接绘制信息结构体实例(与前面第3行中的数量1对应),并指定了该结构体实例中各项参数的值。细心的读者会发现,这些参数的含义与前面使用顶点法直接绘制时所用vkCmdDraw方法的相关参数含义完全相同
    VkDrawIndirectCommand dic; //构建间接绘制信息结构体实例
	dic.vertexCount = vCount; //顶点数量
	dic.firstInstance = 0; //第一个绘制的实例序号
	dic.firstVertex = 0; //第一个绘制用的顶点索引
	dic.instanceCount = 1; //需要绘制的实例数量
	memcpy(pData, &dic, drawCmdbufbytes); //将数据拷贝进设备内存
	vkUnmapMemory(device, vertexDataMem); //解除内存映射
	result = vkBindBufferMemory(device, drawCmdbuf, drawCmdMem, 0); //绑定内存与缓冲
	assert(result == VK_SUCCESS); //检查绑定是否成功
}

要想成功创建间接绘制信息数据缓冲,还需要在程序中恰当的位置调用上述initDrawCmdbuf方法

//绘制方法

void DrawableObjectCommonLight::drawSelf(VkCommandBuffer& cmd, VkPipelineLayout& pipelineLayout, VkPipeline& pipeline, VkDescriptorSet* desSetPointer)
{
	vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);//将当前使用的命令缓冲与指定管线绑定
	vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, desSetPointer, 0, NULL);//将命令缓冲、管线布局、描述集绑定
	const VkDeviceSize offsetsVertex[1] = { 0 };//顶点数据偏移量数组
	vkCmdBindVertexBuffers(//将顶点数据与当前使用的命令缓冲绑定
		cmd,				//当前使用的命令缓冲
		0,					//顶点数据缓冲在列表中的首索引
		1,					//绑定顶点缓冲的数量
		&(vertexDatabuf),	//绑定的顶点数据缓冲列表
		offsetsVertex		//各个顶点数据缓冲的内部偏移量
	);
	float* mvp = MatrixState3D::getFinalMatrix();					//获取总变换矩阵
	memcpy(pushConstantData, mvp, sizeof(float) * 16);				//将总变换矩阵拷贝入内存
	vkCmdPushConstants(cmd, pipelineLayout, 				//将常量数据送入管线
	VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(float) * 16, pushConstantData);
	//与之前案例不同的是在绘制时不再是调用vkCmdDraw方法,而是调用了vkCmdDrawIndirect方法进行间接绘制。
	vkCmdDrawIndirect(
		cmd, //当前使用的命令缓冲
		drawCmdbuf, //间接绘制信息数据缓冲
		0, //绘制信息数据的起始偏移量
		indirectDrawCount, //此次绘制使用的间接绘制信息组的数量
		sizeof(VkDrawIndirectCommand)); //每组绘制信息数据所占字节数
}

该方法以之前创建的间接绘制信息数据缓冲为入口参数,使GPU可以从该缓冲中获取绘制所需的信息,从而完成绘制工作。

在这里插入图片描述

索引法间接绘制

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值