OpenGl笔记--纹理贴图


一、圆柱,圆盘,圆锥的绘制

绘制一个物体,我们首先得确定其的顶点数据,再将顶点数据送入着色器进行绘制
圆柱的绘制
圆柱是由两个圆形以及一个矩形绘制成的图形,那么我们所要确立的顶点也就显而易见了,那便是上下两个圆的分割点,再由足够多上下四个点构成的两个三角面片绘制成侧面即可。
大致的图片已经贴出如下

代码如下(示例):
在这里插入图片描述

generateCylinder(int num_division, float radius, float height)

我们先来看下获取圆柱顶点函数所需要的参数
1,顶点的个数(这个决定了上下两个圆面的圆滑程度)
2,上下两个圆面的半径
3,上下两个圆片的高度

	int num_samples = num_division;
	float step = 2 * M_PI / num_samples; // 每个切片的弧度

在画圆的时候,如果采用直角坐标的表示法将会特别麻烦,所以这里采用了参数方程(或者极坐标),在圆的方程里,极坐标和参数方程的形式是一样的,所以这里可以随意选一个
那么,我们首先得获取的每个点的度数(代码如上)

	// 按cos和sin生成x,y坐标,z为负,即得到下表面顶点坐标
	// 顶点, 纹理
	float z = -height;
	for (int i = 0; i < num_samples; i++)
	{
		float r_r_r = i * step;
		float x = radius * cos(r_r_r);
		float y = radius * sin(r_r_r);
		// 添加顶点坐标
		vertex_positions.push_back(vec3(x, y, z));
		vertex_normals.push_back( normalize(vec3(x, y, 0)));
		// 这里颜色和法向量一样
		vertex_colors.push_back( normalize(vec3(x, y, 0)));
	}

	// 按cos和sin生成x,y坐标,z为正,即得到上表面顶点坐标
	z = height;
	for (int i = 0; i < num_samples; i++)
	{
		float r_r_r = i * step;
		float x = radius * cos(r_r_r);
		float y = radius * sin(r_r_r);
		vertex_positions.push_back(vec3(x, y, z));
		vertex_normals.push_back( normalize(vec3(x, y, 0)));
		vertex_colors.push_back( normalize(vec3(x, y, 0)));
	}

再由极坐标公式可得到每个顶点的顶点坐标,获取后将其压进vector_position即可(这个容器类是由vector定义的,详情使用方法可自行百度),其余两个分别是存颜色和法向量的容器(用法类似)
由于有上下两个表面,所以这里得获取到上下两个圆面的坐标,在opengl中的中点坐标为0,所以上下两个高度分别为正负height

面片的获取

		for (int i = 0; i < num_samples; i++)
	{
		// 面片1
		faces.push_back(vec3i(i, (i + 1) % num_samples, (i) + num_samples));
		// 面片1对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back( vec3i( 6*i, 6*i+1, 6*i+2 ) );


		// 面片2
		faces.push_back(vec3i((i) + num_samples, (i + 1) % num_samples, (i + num_samples + 1) % (num_samples) + num_samples));
		// 面片2对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
		vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 1.0));
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back( vec3i( 6*i+3, 6*i+4, 6*i+5 ) );
	}

每三个点组成一个三角面片,这里i,(i+1)%num_sample,(i)+num_samples代表的顶点的序号(即下标,因为顶点以及被存进了vector里)
为什么要取余
因为当我们取到第num_sample个(最大序号)点时,num_sample+1点越界了,而我们实际上想得到的下一个点应该是取回第一个点,所以这里应当使用取余操作
i+num_sample
因为我们所要的面片是以2+1上面2个点,下面一个点的形式获取,那么i+num_sample便显而易见了,那便是i点所对应的的下表面的一个点
这样,我们便获取到了所需要的一个面片
在这里插入图片描述
下面的vertex_texture则是纹理坐标的获取,这个将于下文中进行阐述
完整代码如下:

void TriMesh::generateCylinder(int num_division, float radius, float height)
{

	cleanData();

	int num_samples = num_division;
	float step = 2 * M_PI / num_samples; // 每个切片的弧度

	// 按cos和sin生成x,y坐标,z为负,即得到下表面顶点坐标
	// 顶点, 纹理
	float z = -height;
	for (int i = 0; i < num_samples; i++)
	{
		float r_r_r = i * step;
		float x = radius * cos(r_r_r);
		float y = radius * sin(r_r_r);
		// 添加顶点坐标
		vertex_positions.push_back(vec3(x, y, z));
		vertex_normals.push_back( normalize(vec3(x, y, 0)));
		// 这里颜色和法向量一样
		vertex_colors.push_back( normalize(vec3(x, y, 0)));
	}

	// 按cos和sin生成x,y坐标,z为正,即得到上表面顶点坐标
	z = height;
	for (int i = 0; i < num_samples; i++)
	{
		float r_r_r = i * step;
		float x = radius * cos(r_r_r);
		float y = radius * sin(r_r_r);
		vertex_positions.push_back(vec3(x, y, z));
		vertex_normals.push_back( normalize(vec3(x, y, 0)));
		vertex_colors.push_back( normalize(vec3(x, y, 0)));
	}

	// 面片生成三角面片,每个矩形由两个三角形面片构成
	for (int i = 0; i < num_samples; i++)
	{
		// 面片1
		faces.push_back(vec3i(i, (i + 1) % num_samples, (i) + num_samples));
		// 面片1对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back( vec3i( 6*i, 6*i+1, 6*i+2 ) );


		// 面片2
		faces.push_back(vec3i((i) + num_samples, (i + 1) % num_samples, (i + num_samples + 1) % (num_samples) + num_samples));
		// 面片2对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
		vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 1.0));
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back( vec3i( 6*i+3, 6*i+4, 6*i+5 ) );
	}

	// 三角面片的每个顶点的法向量的下标,这里和顶点坐标的下标 faces是一致的,所以我们用faces就行
	normal_index = faces;
	// 三角面片的每个顶点的颜色的下标
	color_index = faces;

	storeFacesPoints();
}

圆盘的绘制
通过圆柱的绘制方式,我们不难猜想到,圆盘的绘制是由一个中心点以及圆周的分割点,以一个圆心+两个边上的点形成的三角面片所组成

void TriMesh::generateDisk(int num_division, float radius)

函数的定义类似,但在这里我们少了一个参数,那便是高度,这里的绘制方法并不是一个三维物体的绘制方法,而是以一个二维的圆片来近似的,那么我们便不再需要高度

	int num_samples = num_division;
	float step = 2 * M_PI / num_samples; // 每个切片的弧度
	//先压原点
	
	for (int i = 0; i < num_samples; i++)
	{
		float r_r_r = i * step;
		float x = radius * cos(r_r_r)+0.5;
		float z = radius * sin(r_r_r);
		vertex_positions.push_back(vec3(x,0, z));
		vertex_normals.push_back(normalize(vec3(x, 0, z)));
		vertex_colors.push_back(normalize(vec3(x, 0, z)));
	}
	vec3 pos(0.5, 0, 0);
	vertex_positions.push_back(pos);
	vertex_normals.push_back(normalize(pos));
	vertex_colors.push_back(normalize(pos));

获取圆上顶点的方法以及在上面圆柱给出,这里只需要把y设置为0,z代替原来的y来实现即可(因为物体是置于y=0这个平面的),这里有一个细节(女生所要男生的细节无处不在),那便是圆盘的中心点坐标我们是最后存进vector里面的,因为我们如果是在第一个存,那么他的下标便是0,当我们在取余数返回第一个点时就会出现错误,所以我们应当把中心点在最后存储

	for (int i = 0; i < num_samples; i++)
	{
		float r_r_rs = (i + 1) % num_samples * step;
		float r_r_r = i * step;
		// 面片1
		faces.push_back(vec3i(num_samples, (i + 1) % num_samples, (i) % num_samples));
		// 面片1对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(0.5, 0.5));
		vertex_textures.push_back(vec2(0.5 * cos(r_r_rs) + 0.5, 0.5 * sin(r_r_rs)+ 0.5));
		vertex_textures.push_back(vec2(0.5 * cos(r_r_r) + 0.5, 0.5 * sin(r_r_r) + 0.5));//利用到上面的弧度
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back(vec3i(3 * i, 3 * i + 1, 3 * i + 2));//循环一次压进去3个顶点坐标

	}

获取方法也和圆柱类似,重点的是我们的第一个值不能变,保持为num_sample,,因为第一个点永远是我们的圆心坐标在数组里的下标,对应第四行代码

完整代码

void TriMesh::generateDisk(int num_division, float radius)
{
	cleanData();
	// @TODO:  Task2 请在此添加代码生成圆盘
	int num_samples = num_division;
	float step = 2 * M_PI / num_samples; // 每个切片的弧度
	//先压原点
	
	for (int i = 0; i < num_samples; i++)
	{
		float r_r_r = i * step;
		float x = radius * cos(r_r_r)+0.5;
		float z = radius * sin(r_r_r);
		vertex_positions.push_back(vec3(x,0, z));
		vertex_normals.push_back(normalize(vec3(x, 0, z)));
		vertex_colors.push_back(normalize(vec3(x, 0, z)));
	}
	vec3 pos(0.5, 0, 0);
	vertex_positions.push_back(pos);
	vertex_normals.push_back(normalize(pos));
	vertex_colors.push_back(normalize(pos));
	// 面片生成三角面片,每个矩形由两个三角形面片构成
	for (int i = 0; i < num_samples; i++)
	{
		float r_r_rs = (i + 1) % num_samples * step;
		float r_r_r = i * step;
		// 面片1
		faces.push_back(vec3i(num_samples, (i + 1) % num_samples, (i) % num_samples));
		// 面片1对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(0.5, 0.5));
		vertex_textures.push_back(vec2(0.5 * cos(r_r_rs) + 0.5, 0.5 * sin(r_r_rs)+ 0.5));
		vertex_textures.push_back(vec2(0.5 * cos(r_r_r) + 0.5, 0.5 * sin(r_r_r) + 0.5));//利用到上面的弧度
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back(vec3i(3 * i, 3 * i + 1, 3 * i + 2));//循环一次压进去3个顶点坐标

	}


	// 三角面片的每个顶点的法向量的下标,这里和顶点坐标的下标 faces是不一致的,要修改
	normal_index = faces;
	// 三角面片的每个顶点的颜色的下标
	color_index = faces;
	
	storeFacesPoints();
}

圆锥的绘制
圆锥的绘制则是在圆柱的基础上,上表面改为一个点即可
由数学知识可得,圆锥的展开图便是一个扇形,那么我们只要用上顶点作为圆心,按照上面圆的绘制方法即可完成绘制
这里就直接贴完整代码了

void TriMesh::generateCone(int num_division, float radius, float height)
{
	cleanData();
	// @TODO: Task2 请在此添加代码生成圆锥体
	int num_samples = num_division;
	float step = 2 * M_PI / num_samples;

	// 顶点, 纹理
	float z = height;
	for (int i = 0; i < num_samples; i++)
	{
		float r_r_r = i * step;
		float x = radius * cos(r_r_r)+0.8;
		float y = radius * sin(r_r_r)+0.8;
		// 添加顶点坐标
		vertex_positions.push_back(vec3(x, y, z));
		vertex_normals.push_back(normalize(vec3(x, y, 0)));
		// 这里颜色和法向量一样
		vertex_colors.push_back(normalize(vec3(x, y, 0)));
	}

	// 按cos和sin生成x,y坐标,z为正,即得到上表面顶点坐标
	z = -height;
	
		vertex_positions.push_back(vec3(0.75, 0.75, z));
		vertex_normals.push_back(normalize(vec3(0.75, 0.75, 0)));
		vertex_colors.push_back(normalize(vec3(0.75, 0.75, 0)));
	//上表面只有一个点


	// 面片生成三角面片,每个矩形由两个三角形面片构成
	for (int i = 0; i < num_samples; i++)
	{
		// 面片1
		faces.push_back(vec3i(num_samples, (i + 1) % num_samples, (i)%num_samples));
		// 面片1对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(0.5, 1.0));
		vertex_textures.push_back(vec2(1.0 * (i + 1) / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 0.0));
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back(vec3i(3 * i, 3 * i + 1, 3* i + 2));


	}

	// 三角面片的每个顶点的法向量的下标,这里和顶点坐标的下标 faces是一致的,所以我们用faces就行
	normal_index = faces;
	// 三角面片的每个顶点的颜色的下标
	color_index = faces;

	storeFacesPoints();
}

二、纹理贴图

1.什么是纹理

简单的说,上面我们画了三个几何体,那么纹理就是让我们把三张照片贴到上面几个几何体上

所以第一步便是获取我们的图片

代码如下(示例):

painter->addMesh(cylinder, "mesh_a", "./assets/cylinder10.jpg", vshader, fshader); 	// 指定纹理与着色器

这里我们封装了addMesh这个函数,因为我们不可能只用一个单一的纹理

void MeshPainter::addMesh( TriMesh* mesh, const std::string &name, const std::string &texture_image, const std::string &vshader, const std::string &fshader ){
	mesh_names.push_back(name);
    meshes.push_back(mesh);

    openGLObject object;
    // 绑定openGL对象,并传递顶点属性的数据
    bindObjectAndData(mesh, object, texture_image, vshader, fshader);

    opengl_objects.push_back(object);
};

这里我们丢进去了一个物体类,一张图片,顶点着色器,还有片元着色器
通过数据绑定函数将物体与纹理绑定起来

bing函数的实现

void MeshPainter::bindObjectAndData(TriMesh *mesh, openGLObject &object, const std::string &texture_image, const std::string &vshader, const std::string &fshader){
    // 初始化各种对象

    std::vector<vec3> points = mesh->getPoints();
    std::vector<vec3> normals = mesh->getNormals();
    std::vector<vec3> colors = mesh->getColors();
    std::vector<vec2> textures = mesh->getTextures();

	// 创建顶点数组对象
#ifdef __APPLE__	// for MacOS
	glGenVertexArraysAPPLE(1, &object.vao);		// 分配1个顶点数组对象
	glBindVertexArrayAPPLE(object.vao);		// 绑定顶点数组对象
#else				// for Windows
	glGenVertexArrays(1, &object.vao);  	// 分配1个顶点数组对象
	glBindVertexArray(object.vao);  	// 绑定顶点数组对象
#endif

	// 创建并初始化顶点缓存对象
	glGenBuffers(1, &object.vbo);
	glBindBuffer(GL_ARRAY_BUFFER, object.vbo);
    glBufferData(GL_ARRAY_BUFFER,
                 points.size() * sizeof(vec3) +
                     normals.size() * sizeof(vec3) +
                     colors.size() * sizeof(vec3) +
                     textures.size() * sizeof(vec2),
                 NULL, GL_STATIC_DRAW);

    // 绑定顶点数据
    glBufferSubData(GL_ARRAY_BUFFER, 0, points.size() * sizeof(vec3), points.data());
    // 绑定颜色数据
    glBufferSubData(GL_ARRAY_BUFFER, points.size() * sizeof(vec3), colors.size() * sizeof(vec3), colors.data());
    // 绑定法向量数据
    glBufferSubData(GL_ARRAY_BUFFER, (points.size() + colors.size()) * sizeof(vec3), normals.size() * sizeof(vec3), normals.data());
    // 绑定纹理数据
    glBufferSubData(GL_ARRAY_BUFFER, (points.size() + normals.size() + colors.size()) * sizeof(vec3), textures.size() * sizeof(vec2), textures.data());


	object.vshader = vshader;
	object.fshader = fshader;
	object.program = InitShader(object.vshader.c_str(), object.fshader.c_str());

    // 将顶点传入着色器
	object.pLocation = glGetAttribLocation(object.program, "vPosition");
	glEnableVertexAttribArray(object.pLocation);
	glVertexAttribPointer(object.pLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));

    // 将颜色传入着色器
	object.cLocation = glGetAttribLocation(object.program, "vColor");
	glEnableVertexAttribArray(object.cLocation);
	glVertexAttribPointer(object.cLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(points.size() * sizeof(vec3)));

    // 将法向量传入着色器
	object.nLocation = glGetAttribLocation(object.program, "vNormal");
	glEnableVertexAttribArray(object.nLocation);
	glVertexAttribPointer(object.nLocation, 3, 
		GL_FLOAT, GL_FALSE, 0, 
		BUFFER_OFFSET( (points.size() + colors.size())  * sizeof(vec3)));

    // @TODO: Task1 将纹理坐标传入着色器
    object.tLocation = glGetAttribLocation(object.program, "vTexture");
    glEnableVertexAttribArray(object.tLocation);
    glVertexAttribPointer(object.tLocation, 2,
        GL_FLOAT, GL_FALSE, 0,
        BUFFER_OFFSET((points.size() + colors.size()+ normals.size()) * sizeof(vec3)));


	// 获得矩阵位置
	object.modelLocation = glGetUniformLocation(object.program, "model");
	object.viewLocation = glGetUniformLocation(object.program, "view");
	object.projectionLocation = glGetUniformLocation(object.program, "projection");

	object.shadowLocation = glGetUniformLocation(object.program, "isShadow");

    // 读取纹理图片数
    object.texture_image = texture_image;
    // 创建纹理的缓存对象
    glGenTextures(1, &object.texture);
    // 调用stb_image生成纹理
    load_texture_STBImage(object.texture_image, object.texture);
    
    // Clean up
    glUseProgram(0);
#ifdef __APPLE__	// for MacOS
	glBindVertexArrayAPPLE(0);
#else				// for Windows
	glBindVertexArray(0);
#endif

};

这分别是顶点数据的绑定以及投影矩阵的绑定等等,但这些不是这期文章的重要内容

painter->addMesh(cylinder, "mesh_a", "./assets/cylinder10.jpg", vshader, fshader); 	// 指定纹理与着色器
    // @TODO: Task1 将纹理坐标传入着色器
    object.tLocation = glGetAttribLocation(object.program, "vTexture");
    glEnableVertexAttribArray(object.tLocation);
    glVertexAttribPointer(object.tLocation, 2,
        GL_FLOAT, GL_FALSE, 0,
        BUFFER_OFFSET((points.size() + colors.size()+ normals.size()) * sizeof(vec3)));

重点在这行代码,我们先在openglobject里定义一个tlocation变量,这个变量用于和着色器变量“vTexture”通过glgetattriblocation函数进行绑定,然后再通过glVertexAttribPointer函数存进缓冲对象即可,因为纹理只是一张图片,是二维的,所以相应的in变量也应该是vec2形式的,所以这里的第二个参数要改为2,这样便完成了纹理变量的绑定

2.纹理坐标的映射

(0,0) (1,1)
在这里插入图片描述
一张纹理图片,有着它自己的坐标系,
而我们在将它贴到我们所绘制的几何体中时,几何体也有着它的坐标系,那么我们就得实现两个坐标点之间的转换
圆柱纹理坐标

// 面片生成三角面片,每个矩形由两个三角形面片构成
	for (int i = 0; i < num_samples; i++)
	{
		// 面片1
		faces.push_back(vec3i(i, (i + 1) % num_samples, (i) + num_samples));
		// 面片1对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back( vec3i( 6*i, 6*i+1, 6*i+2 ) );


		// 面片2
		faces.push_back(vec3i((i) + num_samples, (i + 1) % num_samples, (i + num_samples + 1) % (num_samples) + num_samples));
		// 面片2对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
		vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 1.0));
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back( vec3i( 6*i+3, 6*i+4, 6*i+5 ) );
	}

三个纹理坐标,分别代表xyz轴,x对应为(i,0)是指把图片上(i,0)点的图片信息送到x点,其余类似,为什么最后要(i+6)呢,由上下可以看出,我们一次是存了6个纹理坐标的,所以这里应当是6
圆盘纹理坐标

	for (int i = 0; i < num_samples; i++)
	{
		float r_r_rs = (i + 1) % num_samples * step;
		float r_r_r = i * step;
		// 面片1
		faces.push_back(vec3i(num_samples, (i + 1) % num_samples, (i) % num_samples));
		// 面片1对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(0.5, 0.5));
		vertex_textures.push_back(vec2(0.5 * cos(r_r_rs) + 0.5, 0.5 * sin(r_r_rs)+ 0.5));
		vertex_textures.push_back(vec2(0.5 * cos(r_r_r) + 0.5, 0.5 * sin(r_r_r) + 0.5));//利用到上面的弧度
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back(vec3i(3 * i, 3 * i + 1, 3 * i + 2));//循环一次压进去3个顶点坐标

	}

我们画的图是一个圆,我们要贴的图也是一个圆,这两个圆的区别在哪里?
没错,就是圆心的位置以及半径的不同,那么我们只需要在原cos,sin的基础上把半径换成图片的半径,位置换到中心点即可实现,注意
1,这里是只有三个纹理坐标,所以i只需要加3即可
2,第一个纹理的坐标即x,永远是圆心,这是不能改变的

圆锥纹理坐标
圆锥的纹理坐标和圆类似,但是我们可以同将下面的点近似为直线来进行简化操作

	for (int i = 0; i < num_samples; i++)
	{
		// 面片1
		faces.push_back(vec3i(num_samples, (i + 1) % num_samples, (i)%num_samples));
		// 面片1对应的顶点的纹理坐标
		vertex_textures.push_back(vec2(0.5, 1.0));
		vertex_textures.push_back(vec2(1.0 * (i + 1) / num_samples, 0.0));
		vertex_textures.push_back(vec2(1.0 * i / num_samples, 0.0));
		// 对应的三角面片的纹理坐标的下标
		texture_index.push_back(vec3i(3 * i, 3 * i + 1, 3* i + 2));


	}

这里便是综合了圆以及矩形的顶点方法
第一个点存圆锥的上顶点
下面便按i+1,i一字排开即可实现

3.效果图

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值