Directx11地形渲染教程二十一之CubeMap实现SphereReflection(球面反射)

在3D图形学中,CubeMap的使用有多种通途,其中一种用作SkyDome(天空盒子)的渲染,参考上节教程D3D11地形渲染教程二十之CubeMap实现SkyDome(天空盒),而CubeMap的另外一种用途是作为EnvironmentMap的ReflectTexture(反射纹理)来使用的.本节教程的代码结构和上节教程是一样的,如下:





一,SphereReflection(球面反射)

在3D游戏中,假设场景存在一个玻璃球,那么整个3D场景由于反射现象倒映到球体表面,这是一种球面反射现象,可以用下面的图来说明原理



这让我们想起了前面DX11入门教程系列的 D3D11教程十九之PlannarReflection(基于RTT技术和投影纹理技术),利用RT(渲染到纹理)来实现平面反射,当然球面反射跟平面反射是不太一样的,因为球面不是平的,球面反射将四周都反射进去了,而平面反射仅仅是一个面在反射而已,这个从日常生活中就能觉察出来了。这里球面反射的实现也跟那节教程的平面反射一样借用了反射RT,但是本节教程的反射RT并非普通的2D纹理,而是CubeMap(立方体纹理),具备6个反射面,分别是照相机沿着各个坐标轴渲染的反射RT,最终融合成一张CubeMap,也就是我们往+X,-X,+Y,-Y,+Z,-Z设置相机LookAt方向,进行渲染得到的6张反射RT,然后合成一张CubeMap,如下面如所示:(为方便表示,用XY平面截图表示)




实现球面反射的步骤具体如下:
(1)设置渲染目的地为反射RT(注意这次的RT类跟平面反射教程的RT类不太一样,注意某些变量的区别)。
(2)分别设置各个方向的相机变换矩阵,渲染整个场景,因为有6个方向的反射相机,所以得渲染整个场景6次。
(3)设置渲染目的地为背后缓存(BackBuffer).
(4)将"(2)"得到的CubeMap给球面进行渲染,与上节教程渲染SkyDome的步骤差不多,只是不用关闭背面剔除。





二,代码实现:

其中RT类的某些变量某些填充参数得仔细注意,代码有注释,好好揣摩,我就不啰嗦了。


(1)RT类代码:


CubeMapRenderModelToTexure.h

#pragma once
#ifndef _CUBE_MAP_RENDER_MODEL_TO_TEXTURE_H
#define _CUBE_MAP_RENDER_MODEL_TO_TEXTURE_H

#include<Windows.h>
#include<xnamath.h>
#include<D3D11.h>
#include"Macro.h"
class CubeMapRenderModelToTextureClass
{
private:

	ID3D11Texture2D* mRenderTargetTexture;
	//CubeMap六个面,所以六个渲染目的视图
	ID3D11RenderTargetView* mRenderTargetView[6];
	ID3D11ShaderResourceView* mShaderResourceView;

	//---由于渲染目标的宽度和高度大小可能跟原来的屏幕不一样,这五个在RenderModelToTextureClass类中重新创建并且设定
	D3D11_VIEWPORT md3dViewport;//视口
	ID3D11Texture2D* mDepthStencilBuffer;  //深度模板缓存
	ID3D11DepthStencilView* mDepthStencilView; //深度模板缓存视图
	XMMATRIX mProjMatrix; //投影矩阵

	int mCubeMapWidth;
	int mCubeMapHeight;


public:
	CubeMapRenderModelToTextureClass();
	CubeMapRenderModelToTextureClass(const CubeMapRenderModelToTextureClass&other);
	~CubeMapRenderModelToTextureClass();
	
	bool Initialize(ID3D11Device* d3dDevice,int TextureWidth,int TexureHeight, float screenDepth, float screenNear);
	void ShutDown();

	void SetRenderTarget(ID3D11DeviceContext* deviceContext, UINT TargetViewSlot);
	void ClearRenderTarget(ID3D11DeviceContext* deviceContext, float red, float green, float blue, float alpha, UINT TargetViewSlot);
	

	//Get函数
	ID3D11ShaderResourceView* GetShaderResourceView();
	int GetTextureWidth() { return mCubeMapWidth; }
	int GetTextureHeight() { return mCubeMapHeight; }
	XMMATRIX GetProjctionMatrix() { return mProjMatrix; }

   
};
#endif // !_RENDER_3D_MODEL_TO_TEXTURE_H



CubeMapRenderModelToTexure.CPP
#include"CubeMapRenderModelToTexure.h"


CubeMapRenderModelToTextureClass::CubeMapRenderModelToTextureClass()
{
	mRenderTargetTexture = NULL;
	for (int i = 0; i < 6; ++i)
	{
		mRenderTargetView[i] = NULL;
	}
	mShaderResourceView = NULL;
	mDepthStencilBuffer = NULL;
	mDepthStencilView = NULL;
}


CubeMapRenderModelToTextureClass::CubeMapRenderModelToTextureClass(const CubeMapRenderModelToTextureClass&other)
{

}

CubeMapRenderModelToTextureClass::~CubeMapRenderModelToTextureClass()
{

}


bool CubeMapRenderModelToTextureClass::Initialize(ID3D11Device* d3dDevice, int TextureWidth, int TexureHeight, float screenDepth, float screenNear)
{
	//首先,赋值纹理资源的宽度和高度
	mCubeMapWidth = TextureWidth;
    mCubeMapHeight = TexureHeight;

	//第一,填充2D纹理形容结构体,并创建2D渲染纹理
	D3D11_TEXTURE2D_DESC CubeMapTextureDesc;
	ZeroMemory(&CubeMapTextureDesc, sizeof(CubeMapTextureDesc));

	CubeMapTextureDesc.Width = mCubeMapWidth;
	CubeMapTextureDesc.Height = mCubeMapHeight;
	CubeMapTextureDesc.MipLevels = 0;
	CubeMapTextureDesc.ArraySize = 6;  //纹理数组包含6张纹理
	CubeMapTextureDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;  //纹理像素为12个字节
	CubeMapTextureDesc.Usage = D3D11_USAGE_DEFAULT;
	CubeMapTextureDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
	CubeMapTextureDesc.CPUAccessFlags = 0;

	//第一个标志用于CubeMap,第二个标志指定让系统自己产生所有的Mip链
	CubeMapTextureDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE | D3D11_RESOURCE_MISC_GENERATE_MIPS;;
	CubeMapTextureDesc.SampleDesc.Count = 1;   //不使用多重采样抗锯齿
	CubeMapTextureDesc.SampleDesc.Quality = 0;


	HR(d3dDevice->CreateTexture2D(&CubeMapTextureDesc, NULL, &mRenderTargetTexture));

	//第二,填充渲染目标视图形容体,并进行创建目标渲染视图
	D3D11_RENDER_TARGET_VIEW_DESC renderTargetViewDesc;

	renderTargetViewDesc.Format = CubeMapTextureDesc.Format;
	renderTargetViewDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY;     //指明这是一个纹理数组(CubeMap条件下)  
	renderTargetViewDesc.Texture2D.MipSlice = 0;       //每个视图只使用最高层的mip链  
	renderTargetViewDesc.Texture2DArray.ArraySize = 1; //每个视图中针对其中一张纹理
	for (int i = 0; i < 6; ++i)
	{
		//每个视图使用对应的那张纹理  
		//这里指定了纹理数组中的起始索引,因为上面ArraySize指定为1,即只使用一张,  
		//因此这样就单独锁定一个纹理了  
		renderTargetViewDesc.Texture2DArray.FirstArraySlice = i;
		HR(d3dDevice->CreateRenderTargetView(mRenderTargetTexture, &renderTargetViewDesc, &mRenderTargetView[i]));
	}




	//第三,填充着色器资源视图形容体,并进行创建着色器资源视图(CubeMap)
	D3D11_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc;
	shaderResourceViewDesc.Format = CubeMapTextureDesc.Format;
	shaderResourceViewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;            //指定这里将它当作cube map  
	shaderResourceViewDesc.Texture2D.MostDetailedMip = 0;   //指定最精细的mip层,0表示高层  
	shaderResourceViewDesc.Texture2D.MipLevels = -1;        //-1 表示使用其所有的mip链(有多少使用多少)  

	HR(d3dDevice->CreateShaderResourceView(mRenderTargetTexture, &shaderResourceViewDesc, &mShaderResourceView));

	//--------------------------------------------------------------
	//第四,填充2DTexture深度缓存(模板缓存)形容结构体,创建深度缓存(模板缓存)
	//--------------------------------------------------------------
	D3D11_TEXTURE2D_DESC depthStencilDesc;
	ZeroMemory(&depthStencilDesc, sizeof(depthStencilDesc));
	depthStencilDesc.Width = TextureWidth;
	depthStencilDesc.Height = TexureHeight;
	depthStencilDesc.MipLevels = 1;
	depthStencilDesc.ArraySize = 1;
	depthStencilDesc.Format = DXGI_FORMAT_D32_FLOAT;
	depthStencilDesc.SampleDesc.Count = 1;
	depthStencilDesc.SampleDesc.Quality = 0;
	depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
	depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
	depthStencilDesc.CPUAccessFlags = 0;
	depthStencilDesc.MiscFlags = 0;
	HR(d3dDevice->CreateTexture2D(&depthStencilDesc,//要创建的纹理的形容
		0,
		&mDepthStencilBuffer)); //指向深度缓存的指针

	//--------------------------------------------------------------
	//第五,填充深度缓存视图形容结构体,创建深度缓存(模板缓存)视图
	//--------------------------------------------------------------
	D3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc;
	ZeroMemory(&depthStencilViewDesc, sizeof(depthStencilViewDesc));
	depthStencilViewDesc.Format = DXGI_FORMAT_D32_FLOAT;
	depthStencilViewDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
	depthStencilViewDesc.Texture2D.MipSlice = 0;

	HR(d3dDevice->CreateDepthStencilView(
		mDepthStencilBuffer, //我们基于这个深度缓存/漏字板缓存创建一个视图
		&depthStencilViewDesc,
		&mDepthStencilView));//指向深度缓存/漏字板视图的指针


	//第六,设置渲染的视口
	md3dViewport.Width = static_cast<float>(TextureWidth);
	md3dViewport.Height = static_cast<float>(TexureHeight);
	md3dViewport.MinDepth = 0.0f;
	md3dViewport.MaxDepth = 1.0f;
	md3dViewport.TopLeftX = 0.0f;
	md3dViewport.TopLeftY = 0.0f;

	//第七,创建投影矩阵
	float fieldOfView = XM_PIDIV2;
	float screenAspect = (float)TextureWidth/ (float)TexureHeight;
	mProjMatrix = XMMatrixPerspectiveFovLH(fieldOfView, screenAspect, screenNear, screenDepth);


	return true;

}


void CubeMapRenderModelToTextureClass::ShutDown()
{
	ReleaseCOM(mDepthStencilBuffer);
	ReleaseCOM(mDepthStencilView);
	ReleaseCOM(mRenderTargetTexture);
	for (int i = 0; i < 6; ++i)
	{
		ReleaseCOM(mRenderTargetView[i]);
	}
	
	ReleaseCOM(mShaderResourceView);
}


//让此时所有图形渲染到这个目前渲染的位置
void CubeMapRenderModelToTextureClass::SetRenderTarget(ID3D11DeviceContext* deviceContext,UINT TargetViewSlot)
{
	//将渲染目标变为纹理资源视图
	deviceContext->OMSetRenderTargets(1, &mRenderTargetView[TargetViewSlot], mDepthStencilView);

	//设置视口
	deviceContext->RSSetViewports(1, &md3dViewport);
}

void CubeMapRenderModelToTextureClass::ClearRenderTarget(ID3D11DeviceContext* deviceContext,  float red, float green, float blue, float alpha, UINT TargetViewSlot)
{
	//设置清除缓存为的颜色
	float color[4];
	color[0] = red;
	color[1] = green;
	color[2] = blue;
	color[3] = alpha;

	//清除背后缓存
	deviceContext->ClearRenderTargetView(mRenderTargetView[TargetViewSlot], color);

	//清除深度缓存(因为是DXGI_FORMAT_D32_FLOAT,所以这里没模板缓存)
	deviceContext->ClearDepthStencilView(mDepthStencilView, D3D11_CLEAR_DEPTH , 1.0f, 0);
}

// 将“被渲染模型到纹理的纹理”作为ShaderResourceView资源返回,这个资源将会跟其它的ShaderResourceView资源一样被送入Shader里计算.
ID3D11ShaderResourceView* CubeMapRenderModelToTextureClass::GetShaderResourceView()
{
	return mShaderResourceView;
}




(2)反射相机代码:


CubeMapCameraClass.h
#pragma once

#ifndef CUBE_MAP_CAMERA_CLASS_H
#define CUBE_MAP_CAMERA_CLASS_H

#include"FirstCameraClass.h"

class CubeMapCameraClass
{
private:
	FirstCameraClass* mCubeMapCamera[6];
	XMMATRIX mProjMatrix;

public:
	CubeMapCameraClass();
	CubeMapCameraClass(const CubeMapCameraClass&other);
	~CubeMapCameraClass();

public:
	//立方体相机的位置,依据这个位置生成六个相机矩阵
	bool Initilize();
	void BuildCubeMapCamera(float x, float y, float z);
	XMMATRIX GetViewMatrix(UINT slot);
	XMMATRIX GetProjMatrix();
	void SetLens(float fovY, float aspect, float ScreenDepth, float ScreenNear);
	void Shutdown();
};

#endif 


CubeMapCameraClass.h

#include"CubeMapCameraClass.h"

CubeMapCameraClass::CubeMapCameraClass()
{
	for (UINT i = 0; i < 6; ++i)
	{
		mCubeMapCamera[i] = NULL;
	}
}

CubeMapCameraClass::CubeMapCameraClass(const CubeMapCameraClass&other)
{

}

CubeMapCameraClass::~CubeMapCameraClass()
{

}




XMMATRIX CubeMapCameraClass::GetViewMatrix(UINT slot)
{
	return mCubeMapCamera[slot]->GetViewMatrix();
}


bool CubeMapCameraClass::Initilize()
{
	//初始化各个第一人称相机
	for (UINT i = 0; i < 6; ++i)
	{
		mCubeMapCamera[i] = new FirstCameraClass();
		if (!mCubeMapCamera[i])
		{
			return false;
		}
	}

	return true;
}


void CubeMapCameraClass::BuildCubeMapCamera(float x, float y, float z)
{
	//建立立方体贴图相机中心
	XMFLOAT3 center(x, y, z);

	//沿着每条轴观看
	XMFLOAT3 targets[6] =
	{
		XMFLOAT3(x + 1.0f,y,z),
		XMFLOAT3(x -1.0f,y,z),
		XMFLOAT3(x ,y+1.0f,z),
		XMFLOAT3(x ,y-1.0f,z),
		XMFLOAT3(x,y,z+1.0f),
		XMFLOAT3(x ,y,z-1.0f),
	};

	//当往Y或者-Y方向时,Up向量不能为XMFLOAT3(0.0f,1.0f,0.0f)
	XMFLOAT3 ups[6] =
	{
		XMFLOAT3(0.0f,1.0f,0.0f), //+X
		XMFLOAT3(0.0f,1.0f,0.0f), //-X
		XMFLOAT3(0.0f,0.0f,-1.0f), //+Y
		XMFLOAT3(0.0f,0.0f,1.0f), //-Y
		XMFLOAT3(0.0f,1.0f,0.0f), //+Z
		XMFLOAT3(0.0f,1.0f,0.0f), //-Z
	};

	//更新矩阵
	for (UINT i = 0; i < 6; ++i)
	{
		mCubeMapCamera[i]->LookAt(XMLoadFloat3(¢er), XMLoadFloat3(&targets[i]), XMLoadFloat3(&ups[i]));
		mCubeMapCamera[i]->UpdateViewMatrix();
	}

	//建立透视投影矩阵

}

void CubeMapCameraClass::Shutdown()
{
	for (UINT i = 0; i < 6; ++i)
	{
		if (mCubeMapCamera[i])
		{
			delete mCubeMapCamera[i];
			mCubeMapCamera[i] = NULL;
		}
	}
}



void CubeMapCameraClass::SetLens(float fovY, float aspect, float ScreenDepth, float ScreenNear)
{
	mProjMatrix = XMMatrixPerspectiveFovLH(fovY, aspect, ScreenDepth, ScreenNear);
}

XMMATRIX CubeMapCameraClass::GetProjMatrix()
{
	return mProjMatrix;
}


(3)渲染整个场景得到CubeMap的代码.

bool GraphicsClass::RenderSceneToCubeMap()
{
	//建立CubeMapCamera的相机矩阵
	mCubeMapCameraClass->BuildCubeMapCamera(50.0f, 15.0f, 50.0f);
	mCubeMapCameraClass->SetLens(0.5f*XM_PI, 1.0f, SCREEN_FAR, SCREEN_NEAR);

	static float rotate = 0.0f;
	rotate += 0.001f;
	if (rotate >= 360.0f)
	{
		rotate = 0.0f;
	}

	
	for (UINT i = 0; i < 6; ++i)
	{

		XMMATRIX WorldMatrix, ViewMatrix, ProjMatrix;
		bool result;

		//第一,设置渲染目标为第i个RT
		mCubeMapRenderModelToTextureClass->SetRenderTarget(mD3D->GetDeviceContext(), i);

		//第二,清除RT的背后缓存和深度缓存
		mCubeMapRenderModelToTextureClass->ClearRenderTarget(mD3D->GetDeviceContext(), 0.0f, 0.0f, 0.0f, 1.0f, i);

		//第三,获取三个变换矩阵
		WorldMatrix = mD3D->GetWorldMatrix();

		ViewMatrix = mCubeMapCameraClass->GetViewMatrix(i);

		ProjMatrix = mCubeMapCameraClass->GetProjMatrix();


		/*(1)渲染天空*/
		//移动世界矩阵,此时天空的位置应该跟CubeMapCamera的位置一样,或者说是跟ReflectSphere的位置一样,不再是观察相机的位置
		WorldMatrix = WorldMatrix*XMMatrixTranslation(50.0f, 15.0f, 50.0f);

		//关闭背面剔除
		mD3D->TurnOffBackCull();

		//关闭Z缓存测试
		mD3D->TurnOffZBuffer();

		//把天空盒的顶点数据和索引数据放入3D渲染流水线
		mSkyDomeClass->Render(mD3D->GetDeviceContext());

		//用SkyDomeShader进行绘制
		result = mShaderManageClass->RenderSkyDomeShader(mD3D->GetDeviceContext(), mSkyDomeClass->GetIndexCount(), WorldMatrix, ViewMatrix, ProjMatrix, mSkyDomeClass->GetSkyApexColor(), mSkyDomeClass->GetSkyCenterColor(), mSkyDomeClass->GetCubeMap());
		if (!result)
		{
			MessageBox(NULL, L"mSkyDomeShader Render failure", NULL, MB_OK);
			return false;
		}

		//打开背面剔除
		mD3D->TurnOnBackCull();

		//打开Z缓存测试
		mD3D->TurnOnZBuffer();



		/*(2)渲染立方体*/
	    //第一,重置世界矩阵
		WorldMatrix = mD3D->GetWorldMatrix();

		//第二,移动世界矩阵
		WorldMatrix = WorldMatrix*XMMatrixScaling(2.0f, 2.0f, 2.0f)*XMMatrixRotationY(rotate)*XMMatrixTranslation(65.0f, 12.0f, 50.0f);

		//第三,把立方体的顶点数据和索引数据放入3D渲染流水线
		mModelClass->Render(mD3D->GetDeviceContext());

		//第四,用ColorShaderClass进行绘制
		result = mShaderManageClass->RenderColorShader(mD3D->GetDeviceContext(), mModelClass->GetIndexCount(), WorldMatrix, ViewMatrix, ProjMatrix, mTextureClass->GetTexture(), mLightClass->GetDiffuseColor(), mLightClass->GetLightDirection());
		if (!result)
		{
			MessageBox(NULL, L"mColorShaderClass Render failure", NULL, MB_OK);
			return false;
		}
		
	}

	//生成Mips链
	mD3D->GenerateMips(mCubeMapRenderModelToTextureClass->GetShaderResourceView());

	//重新设置渲染目标为背后缓存
	mD3D->SetBackBuffer();

	//重设视口
	mD3D->SetViewport();

	return true;
}


(4)渲染反射球面的Shader代码:
ReflectSphereShader.fx
TextureCube CubeMap:register(t0);  //纹理资源
SamplerState SampleType:register(s0);   //采样方式

//VertexShader
cbuffer CBMatrix:register(b0)
{
	matrix World;
	matrix View;
	matrix Proj;
	matrix WorldInvTranspose;
};


cbuffer CBLight:register(b1)
{
	float4 DiffuseColor;
	float3 LightDirection;
	float pad;
}



struct VertexIn
{
	float3 Pos:POSITION;
	float2 Tex:TEXCOORD0;  //多重纹理可以用其它数字
	float3 Normal:NORMAL;
};



struct VertexOut
{
	float4 Pos:SV_POSITION;
	float3 SkyCubeMapPos:POSITION;
};


VertexOut VS(VertexIn ina)
{
	VertexOut outa;
	outa.Pos = mul(float4(ina.Pos,1.0f), World);
	outa.Pos = mul(outa.Pos, View);
	outa.Pos = mul(outa.Pos, Proj);

	//CubeMap的采样坐标
	outa.SkyCubeMapPos = ina.Pos;


	return outa;
}


float4 PS(VertexOut outa) : SV_Target
{
	float4 color;
    color= CubeMap.Sample(SampleType, outa.SkyCubeMapPos);
	color = saturate(color*1.5f);
	return color;
}




程序运行结果如下:





三,实现球面反射要注意的问题.

(1)渲染SkyDome(天空)到反射RT的时候得注意,此时SkyDome在世界空间的位置不应该是观察相机的位置,而应该是反射球体所在的位置,如果你渲染SkyDome到反射RT使用的为观察相机的位置,将出现"球面黑乎乎一片,大球面显现一个小球面"的不正常渲染结果,如下面截图:









(2)记得在使用CubeMap前,为CubeMap生成Mip链,也就是md3dImmediateContext->GenerateMips(ShaderResourceView),若忘记了这个步骤,渲染结果会不正常,如下面所示:






四,关于优化SphereReflection(球面反射)的思考.

因为为了获取球面反射RT(CubeMap),我们将整个场景渲染了6次,如果我们每帧都进行这样的渲染,毋庸置疑,渲染消耗是巨量的,为此我们可以有各种优化技巧,我随便说个三两条仅供参考。

(1)我们在玩3D游戏中,我们可以不将那些不令人关注的3D模型渲染到反射RT上.
(2)我们在玩3D游戏中,也许不会太注意球面上反射的物体的细节,我们可以在渲染那些物体到反射RT的时候,使用低级的不怎么消耗资源的shader,如我们不使用normalMap,又如一个模型在场景中为10000个面,我们渲染其到反射RT中时我们利用曲面细分(Tesselation)技术只渲染2000个面甚至是1000个面到反射RT上。
(3)由于游戏场景中的一些物体永远是静止,我们考虑是否可以像虚幻四引擎那样用烘焙实现全局光的方式也预先制作一些静态RT,然后我们可以把动态的物体渲染到这张预先渲染了静态物体的反射RT上。

反正优化策略我脑洞大开想到这些,至于能不能实现,还得动手实现以下。还是那句话,优化往往就是性能和显示效果中做个取舍,以及利用人眼观察不到某些画面细节来(trick)偷工减料,哈哈哈。



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值