OpenGL ES渲染之 Shader准备 和 LayerColor

OpenGL ES渲染之     Shader准备


Cocos2dx底层图形绘制是使用OpenGL ES协议的。OpenGL ES是什么呢?

OpenGL ES(OpenGl for Embedded System)是OpenGL三维图形API的子集,针对手机,PDA和游戏主机等嵌入式设备而设计.该API有Khronos集团定义推广,Khronos是一个图形软硬件行业协会,该协会主要关注图形和多媒体方面的开放标准.

OpenGL ES是从OpenGL剪裁或定制过来了,去除了glBegin/glEnd,四边形(GL_QUADS),多边形(GL_POLYGON)等复杂图元等许多非必要的特性.经过多年发展,现在主要有两个版本,OpenGLES1.x针对固定管线硬件,OpenGL ES2.x针对可编程管线硬件.OpenGL ES1.0是以OpenGL1.3规范为基础的,OpenGL ES1.1是以OpenGL1.5为基础的,他们分别又支持common和commonlite两种profile.OpenGL ES2.0是参照OpenGL2.0规范定义的.

 

从Cocos2dx 2.x版本开始,Cocos2dx底层图形渲染使用OpenGL ES2.x新特性可编程着色器(Shader),下面首先介绍Shader的使用流程

xxxxx… //Shader程序

1、创建着色器对象    

glCreateShader

2、着色器对象关联着色器代码     

glShaderSource

3、把着色器源代码编译成目标代码    

glCompileShader

4、验证着色器是否已经变异通过   

glGetShaderiv    glGetShaderInfoLog

5、创建一个着色器程序

glCreatePragram

6、把着色器链接到着色器程序中

glAttachShader

7、链接着色器程序

glLinkProgram

8、验证着色器程序是否链接成功

glGetProgramiv   glGetProgramInfoLog

9、使用着色器程序进行定点或片段处理

glUseProgram

 

在Cocos2dx引擎中GLProgramCache类扮演着一个重要的角色,初始化并且保存Shader程序;并且为需要渲染的元素提供需要的Shader程序;

[cpp]  view plain copy
  1. classCC_DLL GLProgramCache : public Ref  
  2. {  
  3. public:  
  4.     /** 
  5.      * @构造函数 
  6.      */  
  7.     GLProgramCache();  
  8.     /** 
  9.      * @析构函数 
  10.      */  
  11.     ~GLProgramCache();  
  12.    
  13.     /** 单例方法 */  
  14.     static GLProgramCache* getInstance();  
  15.    
  16.     /**清除单例*/  
  17.     static void destroyInstance();  
  18.    
  19.     /**加载Shader程序*/  
  20.     void loadDefaultGLPrograms();  
  21.     CC_DEPRECATED_ATTRIBUTE void loadDefaultShaders(){ loadDefaultGLPrograms(); }  
  22.    
  23.     /**重新加载Shader程序 */  
  24.     void reloadDefaultGLPrograms();  
  25.     CC_DEPRECATED_ATTRIBUTE void reloadDefaultShaders(){ reloadDefaultGLPrograms(); }  
  26.    
  27.     /** 使用Key获取Shader程序 
  28.      */  
  29.     GLProgram * getGLProgram(const std::string &key);  
  30.     CC_DEPRECATED_ATTRIBUTE GLProgram * getProgram(conststd::string &key) { return getGLProgram(key); }  
  31.     CC_DEPRECATED_ATTRIBUTE GLProgram * programForKey(conststd::string &key){ return getGLProgram(key); }  
  32.    
  33.     /** 将Shader程序加入GLProgramCache单例中 */  
  34.     void addGLProgram(GLProgram* program, conststd::string &key);  
  35.     CC_DEPRECATED_ATTRIBUTE void addProgram(GLProgram*program, const std::string &key) { addGLProgram(program, key); }  
  36.    
  37. private:  
  38.     bool init();  
  39.     void loadDefaultGLProgram(GLProgram *program,int type);  
  40.    
  41. //使用字典programs保存所有的Shader程序  
  42.     std::unordered_map<std::string, GLProgram*>_programs;  
  43. };  

下面为单例方法getInstance:

[cpp]  view plain copy
  1. staticGLProgramCache *_sharedGLProgramCache = 0;  
  2. GLProgramCache*GLProgramCache::getInstance()  
  3. {  
  4.     if (!_sharedGLProgramCache) {  
  5.         _sharedGLProgramCache = new GLProgramCache();  
  6.         if (!_sharedGLProgramCache->init())  
  7.         {  
  8.             CC_SAFE_DELETE(_sharedGLProgramCache);  
  9.         }  
  10.     }  
  11.     return _sharedGLProgramCache;  
  12. }  

1、  第一次调用GLProgramCache::getInstance()方法时会new一个GLProgramCache实例方法

2、  初始化GLProgramCache实例方法

3、  方法单例_sharedGLProgramCache

 

下面为GLProgramCache的init方法:

[cpp]  view plain copy
  1. boolGLProgramCache::init()  
  2. {     
  3.     loadDefaultGLPrograms();  
  4.     return true;  
  5. }  
  6. voidGLProgramCache::loadDefaultGLPrograms()  
  7. {  
  8.     GLProgram *p = new GLProgram();  
  9. loadDefaultGLProgram(p, kShaderType_PositionTextureColor);_programs.insert( std::make_pair( GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR,p ) );  
  10. ……  
  11. }  

1、在GLProgramCache::init中会调用加载Shader方法loadDefaultGLPrograms

2、在loadDefaultGLPrograms方法中首先会创建一个GLProgram对象

3、将对应名称的Shader加载到GLProgram对象中

4、将GLProgram对象插入到字典_programs中

 

在loadDefaultGLProgram方法中:

[cpp]  view plain copy
  1. voidGLProgramCache::loadDefaultGLProgram(GLProgram *p, int type)  
  2. {  
  3.     switch (type) {  
  4.         case kShaderType_PositionTextureColor:  
  5.             p->initWithByteArrays(ccPositionTextureColor_vert,ccPositionTextureColor_frag);  
  6.             break;  
  7.         ………  
  8.         default:  
  9.             CCLOG("cocos2d: %s:%d, errorshader type", __FUNCTION__, __LINE__);  
  10.             return;  
  11.     }  
  12.      
  13.     p->link();  
  14.     p->updateUniforms();  
  15.      
  16.     CHECK_GL_ERROR_DEBUG();  
  17. }  

1、 根据GLProgram类型使用对应的shader程序初始化GLProgram;在initWithByteArrays中,会将上述Shader使用流程中1-6不走执行

2、 链接Program

3、 获取该Program中的一些Uniform变量,工后续使用

 

下面看一下Cocos2dx中Shader程序的保存方式:

 

在cocos2d\cocos\renderer\ccShaders.cpp中:

[cpp]  view plain copy
  1. #include"ccShader_Position_uColor.frag"  
  2. #include"ccShader_Position_uColor.vert"  
  3. ……  

ccShader_Position_uColor.vert文件:

[plain]  view plain copy
  1. constchar* ccPosition_uColor_vert = STRINGIFY(  
  2.    
  3. attributevec4 a_position;  
  4. uniformvec4 u_color;  
  5. uniformfloat u_pointSize;  
  6.    
  7. \n#ifdefGL_ES\n  
  8. varyinglowp vec4 v_fragmentColor;  
  9. \n#else\n  
  10. varyingvec4 v_fragmentColor;  
  11. \n#endif\n  
  12.    
  13. voidmain()  
  14. {  
  15.     gl_Position = CC_MVPMatrix * a_position;  
  16.     gl_PointSize = u_pointSize;  
  17.     v_fragmentColor = u_color;  
  18. }  
  19. );  

这里定义了ccPosition_uColor_vert变量,该顶点着色器的功能室使用矩阵计算OpenGL中顶点的位置;

 

ccShader_Position_uColor.frag文件:

[plain]  view plain copy
  1. constchar* ccPosition_uColor_frag = STRINGIFY(  
  2.    
  3. \n#ifdefGL_ES\n  
  4. precisionlowp float;  
  5. \n#endif\n  
  6.    
  7. varyingvec4 v_fragmentColor;  
  8.    
  9. voidmain()  
  10. {  
  11.     gl_FragColor = v_fragmentColor;  
  12. }  
  13. );  

这里定义了ccPosition_uColor_frag变量,该片段Shader的功能就是设置顶点的颜色;

 

上面两段Shader程序会字符串的形式传入initWithByteArrays方法中,下面为initWithByteArrays方法:

[cpp]  view plain copy
  1. boolGLProgram::initWithByteArrays(const GLchar* vShaderByteArray, const GLchar* fShaderByteArray)  
  2. {  
  3.     ……//Windows平台单独设定  
  4.     _program = glCreateProgram();  
  5.     CHECK_GL_ERROR_DEBUG();  
  6.    
  7.     _vertShader = _fragShader = 0;  
  8.    
  9.     if (vShaderByteArray)  
  10.     {  
  11.         if (!compileShader(&_vertShader, GL_VERTEX_SHADER,vShaderByteArray))  
  12.         {  
  13.             CCLOG("cocos2d: ERROR: Failedto compile vertex shader");  
  14.             return false;  
  15.        }  
  16.     }  
  17.    
  18.     // Create and compile fragment shader  
  19.     if (fShaderByteArray)  
  20.     {  
  21.         if (!compileShader(&_fragShader, GL_FRAGMENT_SHADER,fShaderByteArray))  
  22.         {  
  23.             CCLOG("cocos2d: ERROR: Failedto compile fragment shader");  
  24.             return false;  
  25.         }  
  26.     }  
  27.    
  28.     if (_vertShader)  
  29.     {  
  30.         glAttachShader(_program, _vertShader);  
  31.     }  
  32.     CHECK_GL_ERROR_DEBUG();  
  33.    
  34.     if (_fragShader)  
  35.     {  
  36.         glAttachShader(_program, _fragShader);  
  37.     }  
  38.     _hashForUniforms = nullptr;  
  39.      
  40.     CHECK_GL_ERROR_DEBUG();  
  41.     ……//Windows平台单独设定  
  42.     return true;  
  43. }  

1、如果顶点Shader不为空,编译顶点Shader

2、如果片段Shader不为空,编译片段Shader

3、将program和顶点Shader绑定

4、将program和片段Shader绑定

 

在compileShader方法中:

[html]  view plain copy
  1. boolGLProgram::compileShader(GLuint * shader, GLenum type, const GLchar* source)  
  2. {  
  3.     GLint status;  
  4.     if (!source)  return false;     
  5. const GLchar *sources[] = {  
  6.    ……//特殊平台需要的Uniform变量  
  7.         "uniform mat4 CC_PMatrix;\n"  
  8.         "uniform mat4 CC_MVMatrix;\n"  
  9.         "uniform mat4CC_MVPMatrix;\n"  
  10.         "uniform vec4 CC_Time;\n"  
  11.         "uniform vec4 CC_SinTime;\n"  
  12.         "uniform vec4 CC_CosTime;\n"  
  13.         "uniform vec4 CC_Random01;\n"  
  14.         "uniform sampler2DCC_Texture0;\n"  
  15.         "uniform sampler2DCC_Texture1;\n"  
  16.         "uniform sampler2DCC_Texture2;\n"  
  17.         "uniform sampler2DCC_Texture3;\n"  
  18.         "//CC INCLUDES END\n\n",  
  19.         source,  
  20.     };  
  21.    
  22.     *shader = glCreateShader(type);  
  23.     glShaderSource(*shader, sizeof(sources)/sizeof(*sources),sources, nullptr);  
  24.     glCompileShader(*shader);  
  25.    
  26.     glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);  
  27.     if (! status) {  
  28.         GLsizei length;  
  29.         glGetShaderiv(*shader, GL_SHADER_SOURCE_LENGTH,&length);  
  30.         GLchar* src = (GLchar *)malloc(sizeof(GLchar)* length);  
  31.          
  32.         glGetShaderSource(*shader, length, nullptr,src);  
  33.         CCLOG("cocos2d: ERROR: Failed tocompile shader:\n%s", src);  
  34.          
  35.         if (type == GL_VERTEX_SHADER)  
  36.             CCLOG("cocos2d: %s", getVertexShaderLog().c_str());  
  37.         else  
  38.             CCLOG("cocos2d: %s", getFragmentShaderLog().c_str());  
  39.         free(src);  
  40.         return false;;  
  41.     }  
  42.     return (status == GL_TRUE);  
  43. }  

1、在Shader程序字符串之前加入Shader执行时可能需要的Uniform变量,形成新的字符串

2、执行上述Shader使用流程中步骤1-3

3、验证该Shader有没有编译成功

 

此时Cocos2dx中需要使用到的Shader程序都已经准备好了,如何使用后面的博客中会继续讲述;对OpenGL Shader(GLSL)不是很了解的同学可以查询一下这方面的资料。













OpenGL ES渲染之LayerColor


在前面微博中讲述了Cocos2dx引擎OpenGL渲染准备Shader方面,本博客中将使用LayerColor来讲述OpenGL的渲染过程。

 

1、LayerColor对象创建

添加LayerColor元素到游戏中:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. autolayerColor = LayerColor::create(Color4B(255, 0, 0, 255), 100, 100);  
  2. layerColor->setPosition(100,100);  
  3. this->addChild(layerColor);  

下面是LayerColor::create方法:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. LayerColor* LayerColor::create(const Color4B& color, GLfloat width, GLfloat height)  
  2. {  
  3.     LayerColor * layer = new LayerColor();  
  4.     if( layer &&layer->initWithColor(color,width,height)) {  
  5.         layer->autorelease();  
  6.         return layer;  
  7.     }  
  8.     CC_SAFE_DELETE(layer);  
  9.     return nullptr;  
  10. }  

1、使用new操作符创建新LayerColor对象

2、使用initWithColor方法初始化新建的LayerColor对象

3、LayerColor创建并初始化成功后,将该对象加热自动内存管理

 

在LayerColor::initWithColor方法中:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. boolLayerColor::initWithColor(const Color4B& color, GLfloat w, GLfloat h){  
  2.     if (Layer::init()){  
  3.         _blendFunc =BlendFunc::ALPHA_NON_PREMULTIPLIED;  
  4.         _displayedColor.r = _realColor.r =color.r;  
  5.         _displayedColor.g = _realColor.g =color.g;  
  6.         _displayedColor.b = _realColor.b =color.b;  
  7.         _displayedOpacity = _realOpacity =color.a;  
  8.         for (size_t i = 0;i<sizeof(_squareVertices) / sizeof( _squareVertices[0]); i++ )  
  9.         {  
  10.             _squareVertices[i].x = 0.0f;  
  11.             _squareVertices[i].y = 0.0f;  
  12.         }  
  13.    
  14.         updateColor();  
  15.         setContentSize(Size(w, h));  
  16.    
  17.        setGLProgramState(GLProgramState::getOrCreateWithGLProgramName(GLProgram::SHADER_NAME_POSITION_COLOR_NO_MVP));  
  18.         return true;  
  19.     }  
  20.     return false;  
  21. }  

1、调用Layer::init()方法,该方法的主要作用设置默认大小,其实在下面会重新设置LayerColor大小

2、设置混合模式为_blendFunc =BlendFunc::ALPHA_NON_PREMULTIPLIED

3、设置四个顶点的颜色

4、设置四个顶点的坐标,并设置Layer的大小

5、设置LayerColor渲染所使用Shader程序对应的GLProgramState

LayerColor其实是一个四边形,OpenGL会以四边形的方式渲染LayerColor,故需要设置四边形的顶点坐标&顶点颜色;所有的GLProgramState都会保持在GLProgramStateCache::_glProgramStates中,首次获取该GLProgramState时会新建GLProgramState对象,然后将该对象插入到GLProgramStateCache::_glProgramStates中。

 

2、LayerColor渲染

首先先看一下游戏的Function调用流程:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. Application::run()  
  2. {  
  3.     ……  
  4. DisplayLinkDirector::mainLoop()  
  5.     ……  
  6. }  
  7. DisplayLinkDirector::mainLoop()  
  8. {  
  9.     ……  
  10. Director::drawScene()  
  11. ……  
  12. ……  
  13. }  
  14. Director::drawScene()  
  15. {  
  16.     ……  
  17. Node::visit(…)  
  18.     ……  
  19. Renderer::render()  
  20. ……  
  21. }  
  22. Node::visit(…)  
  23. {  
  24.     ……  
  25.     LayerColor::draw(…)  
  26. ……  
  27. }  

LayerColor::draw(…)方法如下:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. voidLayerColor::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)  
  2. {  
  3.     _customCommand.init(_globalZOrder);  
  4.     _customCommand.func =CC_CALLBACK_0(LayerColor::onDraw, this, transform, flags);  
  5.    renderer->addCommand(&_customCommand);  
  6.      
  7.     for(int i = 0; i < 4; ++i)  
  8.     {  
  9.         Vec4 pos;  
  10.         pos.x = _squareVertices[i].x; pos.y =_squareVertices[i].y; pos.z = _positionZ;  
  11.         pos.w = 1;  
  12.        _modelViewTransform.transformVector(&pos);  
  13.         _noMVPVertices[i] =Vec3(pos.x,pos.y,pos.z)/pos.w;  
  14.     }  
  15. }  

1、初始化_customCommand对象,_customCommand是一个渲染指令

2、设置_customCommand的回调函数为LayerColor::onDraw

3、将该LayerColor的渲染指令_customCommand加入到渲染队列中

4、分别将四个顶点坐标通过模型视图矩阵转换

 

Renderer::addCommand(…)方法如下:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. voidRenderer::addCommand(RenderCommand* command)  
  2. {  
  3.     int renderQueue =_commandGroupStack.top();  
  4.     addCommand(command, renderQueue);  
  5. }  
  6. voidRenderer::addCommand(RenderCommand* command, int renderQueue)  
  7. {  
  8.     _renderGroups[renderQueue].push_back(command);  
  9. }  

1、 获取_commandGroupStack的堆栈,其实_commandGroupStack存储的是_renderGroups数组的位置编号

2、 将新的渲染命令加入_renderGroups[renderQueue]堆栈中

 

Renderer::render()方法如下:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. voidRenderer::render()  
  2. {  
  3.     _isRendering = true;  
  4.      
  5.     if (_glViewAssigned)  
  6.     {  
  7.         _drawnBatches = _drawnVertices = 0;  
  8.         for (auto &renderqueue : _renderGroups){  
  9.             renderqueue.sort();  
  10.         }  
  11.         visitRenderQueue(_renderGroups[0]);  
  12.         flush();  
  13.     }  
  14.     clean();  
  15.     _isRendering = false;  
  16. }  

1、将_isRendering该帧是否渲染标志设为true

2、遍历_renderGroups,并对RenderQueue进行排序

3、真正的渲染部分,其实在_renderGroups中只目前只存在一个渲染集合

4、刷新OpenGL中参数

5、将_renderGroups能够所有CommandClean

 

RenderQueue::sort()渲染命令排序方法:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. voidRenderQueue::sort()  
  2. {  
  3.       std::sort(std::begin(_queueNegZ),std::end(_queueNegZ), compareRenderCommand);  
  4.     std::sort(std::begin(_queuePosZ), std::end(_queuePosZ),compareRenderCommand);  
  5. }  
  6. staticbool compareRenderCommand(RenderCommand* a, RenderCommand* b)  
  7. {  
  8.     return a->getGlobalOrder() < b->getGlobalOrder();  
  9. }  

1、将Command中元素Z坐标小于0的Command集合,按照重小到大排序

2、将Command中元素Z坐标大于0的Command集合,按照重小到大排序

 

Renderer::visitRenderQueue方法如下:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. voidRenderer::visitRenderQueue(const RenderQueue& queue)  
  2. {  
  3.     ssize_t size = queue.size();  
  4.      
  5.     for (ssize_t index = 0; index < size; ++index){  
  6.         auto command = queue[index];  
  7.         auto commandType = command->getType();  
  8.         if(RenderCommand::Type::QUAD_COMMAND ==commandType) {  
  9.            ……  
  10.         }else if(RenderCommand::Type::GROUP_COMMAND== commandType) {  
  11.             ……  
  12.         }else if(RenderCommand::Type::CUSTOM_COMMAND== commandType) {  
  13.             ……      
  14.         }else if(RenderCommand::Type::BATCH_COMMAND ==commandType) {  
  15.             ……  
  16.         }else if (RenderCommand::Type::MESH_COMMAND== commandType) {  
  17.             ……  
  18.       }  
  19.     }  
  20. }  

1、获取RenderQueue长度

2、遍历RenderQueue列表,并对不同类型的渲染Command分别处理

3、当渲染Commad类型为QUAD_COMMAND时

4、当渲染Commad类型为GROUP_COMMAND时

5、当渲染Commad类型为CUSTOM_COMMAND时

6、当渲染Commad类型为BATCH_COMMAND时

7、当渲染Commad类型为MESH_COMMAND时

由于篇幅有限,每种渲染的具体流程后续会分别讨论,由于LayerColor使用的是CustomCommand,下面会分析CustomCommand的具体渲染方法

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. elseif(RenderCommand::Type::CUSTOM_COMMAND == commandType)  
  2. {  
  3.     flush();  
  4.     auto cmd = static_cast<CustomCommand*>(command);  
  5.     cmd->execute();  
  6. }  

1、更新OpenGL参数,每次渲染前都需执行的操作,作用是将OpenGL参数设置为默认状态

2、请强制转换类型,并执行渲染Command

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. voidCustomCommand::execute()  
  2. {  
  3.     if(func) {  
  4.         func();  
  5.     }  
  6. }  

判断func是否为空,若func不为空,执行该方法;func是一个回调函数,在添加LayerColor渲染命时指定,如:_customCommand.func= CC_CALLBACK_0(LayerColor::onDraw, this, transform, flags)

 

LayerColor真正的渲染部分:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. voidLayerColor::onDraw(const Mat4& transform, uint32_t flags)  
  2. {  
  3.     getGLProgram()->use();  
  4.     getGLProgram()->setUniformsForBuiltins(transform);  
  5.    
  6. GL::enableVertexAttribs( GL::VERTEX_ATTRIB_FLAG_POSITION| GL::VERTEX_ATTRIB_FLAG_COLOR );  
  7.    
  8.     glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION,3, GL_FLOAT, GL_FALSE, 0, _noMVPVertices);  
  9.     glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR,4, GL_FLOAT, GL_FALSE, 0, _squareColors);  
  10.    
  11.     GL::blendFunc( _blendFunc.src, _blendFunc.dst);  
  12.     glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);  
  13.     CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1,4);  
  14. }  

1、使用对应的Shader程序

2、设置对应的Uniforms 矩阵

3、使能OpenGL中顶点数组和颜色数组功能

4、设置四边形的四个顶点和颜色

5、设置混合模式

6、glDrawArrays画四边形

LayerColor其实是一个有颜色的四边形,渲染LayerColor只需要在正确的位置画一个有颜色的四边形即可;Cocos2dx使用渲染方式是OpenGL ES(Windows平台不同),在OpenGL ES中并没有直接画四边形的方法,故需要借助画三角形的方法画四边形,如glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)为画四边形;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值