结合源码看《我所理解的cocos2dx-3.0》—— 字体

字体

使用的第三方库:FreeType(封装了TrueType)。cocos2dx再使用FontFreeType对其进行封装。

FontFreeType

Create

  1. 根据fontName读取字体文件,并缓存在s_cacheFontData
  2. 第一次时,使用FT_Init_FreeType( &_FTlibrary) 初始化FreeType的FT_Library

FT_Library类对应一个库的单一实例句柄,库对象是所有FT其他对象的容器,开发者需要在做任何事前创建一个新的库实例。通常客户程序通过调用FT_New_Library()方法来创建一个新的库对象。
一个应用程序通常只有一个库实例,销毁它时会销毁它的所有子对象,如face,可以通过FT_DoneFreeType方法销毁库对象,Cocos2d-x在清除Director时销毁库实例。

  1. 调用FT_New_Memory_Face 初始化FT_Face对象
if (FT_New_Memory_Face(getFTLibrary(), s_cacheFontData[fontName].data.getBytes(), s_cacheFontData[fontName].data.getSize(), 0, &face ))
        return false;

一个外观对象对应单个字体外观,即一个特定风格的特定外观类型,例如 Arial和Arial Italic是两个不同的外观。
一个外观对象通常使用 FT_New_Face()来创建,这个函数接收如下参数:一个FT_Library句柄,一个表示字体文件的文件路径,一个决定从文件中装载外观的索引(一个字体文件可能有不同的外观,通常取0),以及FT_Face句柄的地址。它返回一个错误码,如果函数调用成功,则返回0,face参数将被设置成一个非NULL值。
如果已经把字体文件装载到内存,也可以简单地使用FT_New_Memory_Face来创建一个face对象。face对象包含一些用来描述全局字体数据的属性,可以被客户程序直接访问,例如外观中字形的数量、外观家族的名称、风格名称、EM大小等。

  1. 设置字体的字符集,如果设置UNICODE失败了,会遍历一个非FT_ENCODING_NONE的设置。
if (FT_Select_Charmap(face, FT_ENCODING_UNICODE))
{
    int foundIndex = -1;
    for (int charmapIndex = 0; charmapIndex < face->num_charmaps; charmapIndex++)
    {
        if (face->charmaps[charmapIndex]->encoding != FT_ENCODING_NONE)
        {
            foundIndex = charmapIndex;
            break;
        }
    }

    if (foundIndex == -1)
    {
        return false;
    }

    _encoding = face->charmaps[foundIndex]->encoding;
    if (FT_Select_Charmap(face, _encoding))
    {
        return false;
    }
}

FT_CharMap类型用来作为字符映射表对象的句柄,每个FT_CharMap对象包含一个platform和encoding属性,用来标识它对应的字符指令系统。每个字体格式都提供自己的FT_CharMapRec的继承类型并实现它们。在Cocos2d-x中统一使用Unicode编码方式。

  1. 设置font size
// set the requested font size
int dpi = 72;
int fontSizePoints = (int)(64.f * fontSize * CC_CONTENT_SCALE_FACTOR());
if (FT_Set_Char_Size(face, fontSizePoints, fontSizePoints, dpi, dpi))
    return false;

每个FT_Face对象包含一个或多个FT_Size对象,一个size对象用来存放指定字符宽度和高度的特定数据,每个新创建的外观对象有一个尺寸,可以通过face->size直接访问。尺寸对象的内容可以通过调用FT_Set_Pixel_Sizes() 或者 FT_Set_Char_Size() 来改变。例如同样在createFontObject中使用fontSize参数来设置字体大小。

  1. 字形槽 FT_GlyphSlot

字形槽的目的是提供一个地方,可以很容易地一个个地装入字形映像,而不管它的格式(位图、向量轮廓或其他,FreeType也可以加载点阵位图)。理想情况是,一旦一个字形槽创建了,任何字形映像都可以装入,无须其他的内存分配。在实际中,只对于特定格式才如此,像TrueType,它显式地提供数据来计算一个槽的最大尺寸。例如在Cocos2d-x中计算字符精灵表时,使用FT_Load_Glyph来加载字形映像。

Load 字符加载 FT_Load_Char

A function used to load a single glyph into the glyph slot of a    */
  /*    face object, according to its character code.   
  FT_EXPORT( FT_Error )
  FT_Load_Char( FT_Face   face,
                FT_ULong  char_code,
                FT_Int32  load_flags );

其中的参数load_flags决定了加载后字体图像的属性。
这里只对常用的进行解释:
关闭抗锯齿:FT_LOAD_MONOCHROME(这个其实是加载1位bmp位图)
打开抗锯齿:FT_LOAD_FORCE_AUTOHINT
加载时就已经生成图像:FT_LOAD_RENDER
加载时不生成图像:FT_LOAD_NO_BITMAP(注意:这里需要在加载后调用FT_Render_Glyph)

unsigned char* FontFreeType::getGlyphBitmap(uint64_t theChar, long &outWidth, long &outHeight, Rect &outRect,int &xAdvance)
unsigned char* FontFreeType::getGlyphBitmap(uint64_t theChar, long &outWidth, long &outHeight, Rect &outRect,int &xAdvance)
{
    bool invalidChar = true;
    unsigned char* ret = nullptr;

    do
    {
        if (_fontRef == nullptr)
            break;

        if (_distanceFieldEnabled)
        {
            if (FT_Load_Char(_fontRef, static_cast<FT_ULong>(theChar), FT_LOAD_RENDER | FT_LOAD_NO_HINTING | FT_LOAD_NO_AUTOHINT))
                break;
        }
        else
        {
            if (FT_Load_Char(_fontRef, static_cast<FT_ULong>(theChar), FT_LOAD_RENDER | FT_LOAD_NO_AUTOHINT))
                break;
        }

        auto& metrics = _fontRef->glyph->metrics;
        outRect.origin.x = static_cast<float>(metrics.horiBearingX >> 6);
        outRect.origin.y = static_cast<float>(-(metrics.horiBearingY >> 6));
        outRect.size.width = static_cast<float>((metrics.width >> 6));
        outRect.size.height = static_cast<float>((metrics.height >> 6));

        xAdvance = (static_cast<int>(_fontRef->glyph->metrics.horiAdvance >> 6));

        outWidth  = _fontRef->glyph->bitmap.width;
        outHeight = _fontRef->glyph->bitmap.rows;
        ret = _fontRef->glyph->bitmap.buffer;

        if (_outlineSize > 0 && outWidth > 0 && outHeight > 0)
        {
            auto copyBitmap = new (std::nothrow) unsigned char[outWidth * outHeight];
            memcpy(copyBitmap,ret,outWidth * outHeight * sizeof(unsigned char));

            FT_BBox bbox;
            auto outlineBitmap = getGlyphBitmapWithOutline(theChar,bbox);
            if(outlineBitmap == nullptr)
            {
                ret = nullptr;
                delete [] copyBitmap;
                break;
            }

            int glyphMinX = (int)outRect.origin.x;
            int glyphMaxX = (int)(outRect.origin.x + outWidth);
            int glyphMinY = (int)(-outHeight - outRect.origin.y);
            int glyphMaxY = (int)-outRect.origin.y;

            auto outlineMinX = bbox.xMin >> 6;
            auto outlineMaxX = bbox.xMax >> 6;
            auto outlineMinY = bbox.yMin >> 6;
            auto outlineMaxY = bbox.yMax >> 6;
            auto outlineWidth = outlineMaxX - outlineMinX;
            auto outlineHeight = outlineMaxY - outlineMinY;

            auto blendImageMinX = MIN(outlineMinX, glyphMinX);
            auto blendImageMaxY = MAX(outlineMaxY, glyphMaxY);
            auto blendWidth = MAX(outlineMaxX, glyphMaxX) - blendImageMinX;
            auto blendHeight = blendImageMaxY - MIN(outlineMinY, glyphMinY);

            outRect.origin.x = (float)blendImageMinX;
            outRect.origin.y = -blendImageMaxY + _outlineSize;

            unsigned char *blendImage = nullptr;
            if (blendWidth > 0 && blendHeight > 0)
            {
                FT_Pos index, index2;
                auto imageSize = blendWidth * blendHeight * 2;
                blendImage = new (std::nothrow) unsigned char[imageSize];
                memset(blendImage, 0, imageSize);

                auto px = outlineMinX - blendImageMinX;
                auto py = blendImageMaxY - outlineMaxY;
                for (int x = 0; x < outlineWidth; ++x)
                {
                    for (int y = 0; y < outlineHeight; ++y)
                    {
                        index = px + x + ((py + y) * blendWidth);
                        index2 = x + (y * outlineWidth);
                        blendImage[2 * index] = outlineBitmap[index2];
                    }
                }

                px = glyphMinX - blendImageMinX;
                py = blendImageMaxY - glyphMaxY;
                for (int x = 0; x < outWidth; ++x)
                {
                    for (int y = 0; y < outHeight; ++y)
                    {
                        index = px + x + ((y + py) * blendWidth);
                        index2 = x + (y * outWidth);
                        blendImage[2 * index + 1] = copyBitmap[index2];
                    }
                }
            }

            outRect.size.width  = (float)blendWidth;
            outRect.size.height = (float)blendHeight;
            outWidth  = blendWidth;
            outHeight = blendHeight;

            delete [] outlineBitmap;
            delete [] copyBitmap;
            ret = blendImage;
        }

        invalidChar = false;
    } while (0);

    if (invalidChar)
    {
        outRect.size.width  = 0;
        outRect.size.height = 0;
        xAdvance = 0;

        return nullptr;
    }
    else
    {
       return ret;
    }
}

绘制

遍历text的所有字符,调用以下流程(不同text无法合批):

  1. 调用getGlyphBitmap获取单个字符的bmp数据
auto bitmap = _fontFreeType->getGlyphBitmap(it.second, bitmapWidth, bitmapHeight, tempRect, tempDef.xAdvance);
  1. 设置宽高偏移后调用renderCharAt将数据复制给_currentPageData
_fontFreeType->renderCharAt(_currentPageData, (int)_currentPageOrigX + adjustForExtend, (int)_currentPageOrigY + adjustForExtend, bitmap, bitmapWidth, bitmapHeight);
  1. updateTextureContent接口中,使用Texture2DupdateWithData接口更新数据
 _atlasTextures[_currentPage]->updateWithData(data, 0, startY, CacheTextureWidth, (int)_currentPageOrigY - startY + _currLineHeight);
  1. 调用glTexSubImage2D 上传数据

void Texture2DGL::updateSubData(std::size_t xoffset, std::size_t yoffset, std::size_t width, std::size_t height, std::size_t level, uint8_t* data)
{
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, _textureInfo.texture);

    glTexSubImage2D(GL_TEXTURE_2D,
                    level,
                    xoffset,
                    yoffset,
                    width,
                    height,
                    _textureInfo.format,
                    _textureInfo.type,
                    data);
    CHECK_GL_ERROR_DEBUG();

    if(!_hasMipmaps && level > 0)
        _hasMipmaps = true;
}

特效

  1. 阴影(Shadow)是3种特效里实现最简单的一种,同时也是性能最差的一种,它仅将Label分别使用阴影颜色和字体颜色单独绘制一次,参见如下所示的Label的绘制方法。
if (_shadowNode)
{
    _shadowNode->visit(renderer, _modelViewTransform, flags);
}
_textSprite->visit(renderer, _modelViewTransform, flags);
  1. 描边(outline)

Cocos2d-x中的描边(Outline)仅支持轮廓字体,因为它是借助于FreeType 的
FT_Stroker模块来实现的。
FT_Stroker 能够给字形生成一些轮廓信息,并且提供接口判断是否位于轮廓内或者轮廓外。在Cocos2d-x中,如果对Label使用了描边特效,则FontFreeType在初始化的时候会设置FT_Stroker相关的内容。

FontFreeType::FontFreeType(bool distanceFieldEnabled /* = false */, float outline /* = 0 */)
: _fontRef(nullptr)
, _stroker(nullptr)
, _encoding(FT_ENCODING_UNICODE)
, _distanceFieldEnabled(distanceFieldEnabled)
, _outlineSize(0.0f)
, _lineHeight(0)
, _fontAtlas(nullptr)
, _usedGlyphs(GlyphCollection::ASCII)
{
    if (outline > 0.0f)
    {
        _outlineSize = outline * CC_CONTENT_SCALE_FACTOR();
        FT_Stroker_New(FontFreeType::getFTLibrary(), &_stroker);
        FT_Stroker_Set(_stroker,
            (int)(_outlineSize * 64),
            FT_STROKER_LINECAP_ROUND,
            FT_STROKER_LINEJOIN_ROUND,
            0);
    }
}

然后使用getGlyphBitmapWithOutline来获取轮廓信息,其中使用FT_Glyph_StrokerBorder来获取FT_Outline对象,FT_Outline 中则包含了轮廓信息。
与一般情况下使用一个A8格式的纹理来保存一个单通道的Alpha值不同,描边需
要同时保存字体区域的Alpha值和描边区域的Alpha值,在Cocos2d-x中使用一个AI88的纹理来保存带有描边信息的字符精灵表。
在 AI88纹理中,其中一个通道用来保存描边区域,而另一个通道保存字体区域。
FT_Outline包含的区域实际上是包括描边轮廓和字体在内的所有区域,如果用一个单通道的灰度图来表示FT_Outline,则它可以用图13.19上图表示。另一个通道则用来保存字体本身的Alpha通道,它的灰度图如图13.19下图所示。注意,通过对比,同样字号下FT_Outline所占的区域比单独的字体区域要大,这部分用来放置描边区域。
这样,在片段着色器中,描边轮廓内字体区域的Alpha通道值为0,而描边区域的
Alpha通道值为1,可以以此来区分描边区域和字体区域。如下代码是在Cocos2d-x中Label对描边特效使用的片段着色器:

/****************************************************************************
 Copyright (c) 2018-2019 Xiamen Yaji Software Co., Ltd.

 http://www.cocos2d-x.org

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 ****************************************************************************/
 
/*
 * LICENSE ???
 */
const char* labelOutline_frag = R"(
#ifdef GL_ES
precision lowp float;
#endif

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

uniform vec4 u_effectColor;
uniform vec4 u_textColor;
uniform sampler2D u_texture;

#ifdef GL_ES
uniform lowp int u_effectType; // 0: None (Draw text), 1: Outline, 2: Shadow
#else
uniform int u_effectType;
#endif

void main()
{
    vec4 sample = texture2D(u_texture, v_texCoord);
    // fontAlpha == 1 means the area of solid text (without edge)
    // fontAlpha == 0 means the area outside text, including outline area
    // fontAlpha == (0, 1) means the edge of text
    float fontAlpha = sample.a;

    // outlineAlpha == 1 means the area of 'solid text' and 'solid outline'
    // outlineAlpha == 0 means the transparent area outside text and outline
    // outlineAlpha == (0, 1) means the edge of outline
    float outlineAlpha = sample.r;

    if (u_effectType == 0) // draw text
    {
        gl_FragColor = v_fragmentColor * vec4(u_textColor.rgb, u_textColor.a * fontAlpha);
    }
    else if (u_effectType == 1) // draw outline
    {
        // multipy (1.0 - fontAlpha) to make the inner edge of outline smoother and make the text itself transparent.
        gl_FragColor = v_fragmentColor * vec4(u_effectColor.rgb, u_effectColor.a * outlineAlpha * (1.0 - fontAlpha));
    }
    else // draw shadow
    {
        gl_FragColor = v_fragmentColor * vec4(u_effectColor.rgb, u_effectColor.a * outlineAlpha);
    }
}
)";

在该算法中,纹理的A通道表示字体 Alpha值,而R通道表示描边区域Alpha值,
color变量为字体颜色与描边颜色混合后的值。在字体区域,因为字体的透明度为1,所以描边区域不可见。

描边也是增加了renderCommand,使用使用上面的shader,设置不同参数再次绘制

case LabelEffect::OUTLINE:
{
    int effectType = 0;
    Vec4 effectColor(_effectColorF.r, _effectColorF.g, _effectColorF.b, _effectColorF.a);
    
    //draw shadow
    if(_shadowEnabled)
    {
        effectType = 2;
        Vec4 shadowColor = Vec4(_shadowColor4F.r, _shadowColor4F.g, _shadowColor4F.b, _shadowColor4F.a);
        auto *programStateShadow = batch.shadowCommand.getPipelineDescriptor().programState;
        programStateShadow->setUniform(_effectColorLocation, &shadowColor, sizeof(Vec4));
        programStateShadow->setUniform(_effectTypeLocation, &effectType, sizeof(effectType));
        batch.shadowCommand.init(_globalZOrder);
        renderer->addCommand(&batch.shadowCommand);
    }
    
    //draw outline
    {
        effectType = 1;
        updateBuffer(textureAtlas, batch.outLineCommand);
        auto *programStateOutline = batch.outLineCommand.getPipelineDescriptor().programState;
        programStateOutline->setUniform(_effectColorLocation, &effectColor, sizeof(Vec4));
        programStateOutline->setUniform(_effectTypeLocation, &effectType, sizeof(effectType));
        batch.outLineCommand.init(_globalZOrder);
        renderer->addCommand(&batch.outLineCommand);
    }
  
    //draw text
    {
        effectType = 0;
        auto *programStateText= batch.textCommand.getPipelineDescriptor().programState;

        programStateText->setUniform(_effectColorLocation, &effectColor, sizeof(effectColor));
        programStateText->setUniform(_effectTypeLocation, &effectType, sizeof(effectType));
    }
}
  1. 发光
void main()
{
    float dist = texture2D(u_texture, v_texCoord).a;
    //TODO: Implementation 'fwidth' for glsl 1.0
    //float width = fwidth(dist);
    //assign width for constant will lead to a little bit fuzzy,it's temporary measure.
    float width = 0.04;
    float alpha = smoothstep(0.5-width, 0.5+width, dist);
    //glow
    float mu = smoothstep(0.5, 1.0, sqrt(dist));
    vec4 color = u_effectColor*(1.0-alpha) + u_textColor*alpha;
    gl_FragColor = v_fragmentColor * vec4(color.rgb, max(alpha,mu)*color.a);
}

与前面使用Distance Field的片段着色器一样,0.54 以上的区域Alpha值为1,将取
字体本身的颜色,而0~0.5的部分则用来表示发光颜色,用字体颜色混合发光颜色,最
终呈现出正常的发光效果。

架构

image
Label是对所有字体的封装,实际使用通过FontAltasCache管理FontAltasFontAtlasCache使用字体size,描边size,是否使用distanceField作为一个FontAltas的key
snprintf(keyPrefix, ATLAS_MAP_KEY_PREFIX_BUFFER_SIZE, useDistanceField ? "df %.2f %d " : "%.2f %d ", config->fontSize, config->outlineSize);

从上述代码中可以看出,选择一个不同的字体尺寸会导致FontAtlas的重新创建,所以,应用程序中不应该出现多种差别很小的字体尺寸,这会导致多个字体精灵内存的占用。

FontAtlas* FontAtlasCache::getFontAtlasTTF(const _ttfConfig* config)
{
    auto realFontFilename = FileUtils::getInstance()->getNewFilename(config->fontFilePath);  // resolves real file path, to prevent storing multiple atlases for the same file.
    bool useDistanceField = config->distanceFieldEnabled;
    if(config->outlineSize > 0)
    {
        useDistanceField = false;
    }

    std::string key;
    char keyPrefix[ATLAS_MAP_KEY_PREFIX_BUFFER_SIZE];
    snprintf(keyPrefix, ATLAS_MAP_KEY_PREFIX_BUFFER_SIZE, useDistanceField ? "df %.2f %d " : "%.2f %d ", config->fontSize, config->outlineSize);
    std::string atlasName(keyPrefix);
    atlasName += realFontFilename;

    auto it = _atlasMap.find(atlasName);

    if ( it == _atlasMap.end() )
    {
        auto font = FontFreeType::create(realFontFilename, config->fontSize, config->glyphs,
            config->customGlyphs, useDistanceField, (float)config->outlineSize);
        if (font)
        {
            auto tempAtlas = font->createFontAtlas();
            if (tempAtlas)
            {
                _atlasMap[atlasName] = tempAtlas;
                return _atlasMap[atlasName];
            }
        }
    }
    else
        return it->second;

    return nullptr;
}

distance field

Distance filed fonts
讲一讲一种新型的字体渲染方式

Distance Field每个像素记录的不是该像素的Alpha通道值,而是每个像素到文字边缘的距离,例如:

  1. 文字区域外某点的值是它到离它最近的文字区域的距离,这个距离的取值范围
    为0~0.5。
  2. 文字区域内某点的值是它到离它最近的非文字区域的距离,这个距离的取值范
    围为0.5~1.0。
  3. 文字边缘的值为0.5。

有了这个图的数据之后,应用程序就可以使用两种方法来绘制文字。第一种方法使用AIpha测试,这种方法只能在桌面版OpenGL中使用。对渲染管线设置当Alpha 大于0.5时绘制,否则丢弃。由于此时实际渲染并不依赖于图形管线对纹理的缩放,而只是取其缩放后的轮廓(大于0.5的区域),其颜色值不受硬件缩放的影响,所以可以呈现较高的清晰度。但是由于轮廓被线性插值,则会导致一些锯齿。

另一种方法使用自定义的片段着色器来绘制,在片段着色器中可以使用smoothstep方法来使轮廓更平滑

而且因为源数据是离边的距离,两个像素的插值就是当前像素离边的距离。
这个信息是线性的,插值数据不会偏离太远,抗缩放性能非常好。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这份文档提供了FreeType 2函数库设计与实现的细节。本文档的目标是让开发人员更好的理解FreeType 2是如何组织的,并让他们扩充、定制和调试它。 首先,我们先了解这个库的目的,也就是说,为什么会写这个库: * 它让客户应用程序方便的访问字体文件,无论字体文件存储在哪里,并且与字体格式无关。 * 方便的提取全局字体数据,这些数据在平常的字体格式中普遍存在。(例如:全局度量标准,字符编码/字符映射表,等等) * 方便的提取某个字符的字形数据(度量标准,图像,名字,其他任何东西) * 访问字体格式特定的功能(例如,SFNT表,多重控制,OpenType轮廓表) Freetype 2的设计也受如下要求很大的影响: * 高可移植性。这个库必须可以运行在任何环境中。这个要求引入了一些非常激烈的选择,这些是FreeType2的低级系统界面的一部分。 * 可扩展性。新特性应该可以在极少改动库基础代码的前提下添加。这个要求引入了非常简单的设计:几乎所有操作都是以模块的形式提供的。 * 可定制。它应该能够很容易建立一个只包含某个特定项目所需的特性的版本。当你需要集成它到一个嵌入式图形库的字体服务器中时,这是非常重要的。 * 简洁高效。这个库的主要目标是只有很少cpu和内存资源的嵌入式系统。 这份文档的其他部分分为几个部分。首先,一些章节介绍了库的基本设计以及Freetype 2内部对象/数据的管理。 接下来的章节专注于库的定制和与这个话题相关的系统特定的界面,如何写你自己的模块和如何按需裁减库初始化和编译。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值