SDL2 + OPENGL GLSL 实践续一

本文详细介绍了如何在SDL2中利用freetype库生成汉字纹理并结合OpenGL进行显示,包括字体缓存、纹理创建和渲染循环。同时,探讨了GLSL的使用,特别是片元着色器在纹理混合、透明度控制等方面的应用。通过实例展示了如何使用GLSL进行屏幕分屏和绘制动态形状。此外,文章还提及了顶点着色器在图形位置变换中的作用。
摘要由CSDN通过智能技术生成

本篇内容包括:freetype 汉字显示实现,GLSL使用方法,包括片元着色器使用技巧,纹理混合,透明显示方法,纹理采样方法等等,顶点着色器,常用变换技巧,图片放大,图片缩小,图片旋转等等

接上篇:

一、如何在SDL2中加入汉字

SDL没有文字显示功能,要显示文字就必须要将文字图形化,先将文字转化为图形,再通过图形显示功能将其显示出来,OpenGL也是如此。生成文字图形有很多种方法,SDL本身也提供了专用模块,先将文字转化为SDL图形表面,再进行显示。这就是SDL-ttf,该模块实际调用了另外的一套共享软件freetype,OpenGL也有相类似的应用,我研究了一下, 不知道什么原因,sdl-ttf好像从SDL官网中删除了,按照官网的指引从github上下的源程序也不能用,无奈之下,只好用freetype,好在freetype有例子可以参考,网上相关资料也很多,freetype本身没有显示功能,只能先生成字体缓存结构,导入OpenGL或SDL纹理,再通过纹理将文字渲染到帧缓存中,再通过帧缓存将文字显示到屏幕上。其中将字体缓存加入纹理的过程是相当慢的,所以在程序处理过程中,一般会采取预先将要显示的字体写入纹理进行缓存,再进入渲染循环,需要时使用纹理进行显示。因为纹理使用显存和GPU并发处理,处理速率相当快,我测试过,一个包括近百步骤的渲染,也只不过耗时几毫秒。而同样的过程,如果文字实时处理,用时将是之前的十几倍。

也可以预先将要显示的文字做成一个BMP文件,局部就像这样:

使用时预先将字形图像载入生成纹理,如果该文件为1024*1024大小,就可以存4096个32*32的汉字和其他符号或16*16的汉字16384个,足够日常显示之用。这样做的好处是不需要专门的文字处理过程也能完美的显示文字。当然,这需要事先生成字典。

不过在需要在人机交互过程中,临时输入显示没有的文字时,也可以将输入的字典中没有的字插入字典并逐个复制到纹理中,因为人机交互时,不会要求毫秒级的显示速度。

以下是显示文字的页面

以下是有关代码:

map <WCHAR, int > FontMap;
const int  HZSIZE = 32;
const int  ASCIISIZE = 32;
const int  TextBufSize = 32;

int GL_Render::FontInit(string path, INT  isBIG5)
{
    //功能初始化字模纹理
    //输入,游戏文件所在目录
    FT_Library library;
    if (FT_Init_FreeType(&library))
        return 1;
    FT_Face       face;
    if (FT_New_Face(library, "c:/windows/fonts/simfang.ttf", 0, &face))
    {
        FT_Done_FreeType(library);
        return 1;
    }

    //以下初始化字库纹理 
    VOID* gp_TextBuf = NULL;

    FILE* fp;
    string m_path = path + "wor16.asc";
    if ((fopen_s(&fp, m_path.c_str(), "rb")))
        return -1;
    fseek(fp, 0, SEEK_END);
    int nChar = ftell(fp);
    nChar /= 2;
    fseek(fp, 0, SEEK_SET);
    LPWORD charbuf = new WORD[(INT64)nChar + 10];
    if (charbuf == nullptr)
        exit(1);
    fread(charbuf, sizeof(WORD), nChar, fp);

    //建立纹理,缓存存放字模
    if (TextBufSize == 8)
    {
        gp_TextBuf = new char[(size_t)HZSIZE * 64 * ((size_t)nChar / 64 + 1) * HZSIZE];
        memset(gp_TextBuf, 0, (size_t)HZSIZE * 64 * ((size_t)nChar / 64 + 1) * HZSIZE * sizeof(GLubyte));
    }
    else
    {
        gp_TextBuf = new INT32*[(size_t)HZSIZE * 64 * ((size_t)nChar / 64 + 1) * HZSIZE];
        memset(gp_TextBuf, 0, (size_t)HZSIZE * 64 * ((size_t)nChar / 64 + 1) * HZSIZE * sizeof(INT32));
    }
    FT_Set_Pixel_Sizes(face, 0, HZSIZE);

    for (int n = 0; n < nChar; n++)
    {
        char s[3];
        WORD w = charbuf[n];
        WCHAR wstr[2];
        wchar_t rewstr[2];
        s[2] = 0;
        s[0] = w & 0xff;
        s[1] = w >> 8;
        if (isBIG5)
        {
            MultiByteToWideChar(950, 0, s, -1, wstr, 1);
            //转成简体字
            WORD wLCID = MAKELCID(MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED), SORT_CHINESE_BIG5);
            LCMapString(wLCID, LCMAP_SIMPLIFIED_CHINESE, wstr, 1, rewstr, 1);
        }
        else
            MultiByteToWideChar(936, 0, s, -1, rewstr, 1);

        FontMap.insert(pair<WCHAR, INT>(rewstr[0], n));

        rewstr[1] = 0;
        size_t cx = (n & 63) * HZSIZE;
        size_t cy = (n >> 6) * HZSIZE;
        FT_Load_Char(face, rewstr[0], FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL);
        FT_GlyphSlot  slot = face->glyph;
        int xOff = slot->bitmap_left;
        int yOff = (face->size->metrics.ascender >> 6) - slot->bitmap_top;
        if (TextBufSize == 8)
            ConvertSlotToBuf(&slot->bitmap, (GLubyte*)gp_TextBuf, HZSIZE * 64,
                cx  + xOff, cy  + (yOff));
        else 
            ConvertSlotToBuf32(&slot->bitmap, (INT32*)gp_TextBuf, HZSIZE * 64,
                cx  + xOff, cy  + (yOff));
    }
    //复制到纹理
    SDL_Surface* fontsurf;
    if (TextBufSize == 8)
        fontsurf = SDL_CreateRGBSurfaceWithFormatFrom(gp_TextBuf, HZSIZE * 64,
            (nChar / 64 + 1) * HZSIZE, 8,
            HZSIZE * 64, SDL_PIXELFORMAT_RGB332);
    else
        fontsurf = SDL_CreateRGBSurfaceWithFormatFrom(gp_TextBuf, HZSIZE * 64,
            (nChar / 64 + 1) * HZSIZE, 32,
            HZSIZE * 64 * sizeof(INT32), SDL_PIXELFORMAT_RGBA8888);
    //保存字符图
    //SDL_SaveBMP(fontsurf, "fontt.bmp");
    gpFontTexture = SDL_CreateTextureFromSurface(gpSdlRender,fontsurf);
    SDL_FreeSurface(fontsurf);
    delete[]gp_TextBuf;
    delete [] charbuf;
    if (TextBufSize == 8)
    {
        gp_TextBuf = new char[ASCIISIZE * 60 * ASCIISIZE];
        memset(gp_TextBuf, 0, ASCIISIZE * 60 * ASCIISIZE);
    }
    else
    {
        gp_TextBuf = new INT32[ASCIISIZE * 60 * ASCIISIZE];
        memset(gp_TextBuf, 0, ASCIISIZE * 60 * ASCIISIZE * sizeof(INT32));
    }

    FT_Set_Pixel_Sizes(face, 0, ASCIISIZE);

    //打印ascII字符
    for (int n = 0; n < 96; n++)
    {
        WCHAR s[2];
        s[1] = 0;
        s[0] = n + 32;//从空格开始到126
        size_t cx = n * ASCIISIZE / 2;
        //FT_Load_Char(face, s[0], FT_LOAD_RENDER | FT_LOAD_MONOCHROME);
        //FT_Load_Char(face, s[0], FT_LOAD_RENDER|FT_LOAD_TARGET_NORMAL| FT_LOAD_FORCE_AUTOHINT);
        //FT_Load_Char(face, s[0], FT_LOAD_RENDER| FT_LOAD_TARGET_MONO);
        //FT_Load_Char(face, s[0], FT_LOAD_RENDER| FT_LOAD_DEFAULT);
        FT_Load_Char(face, s[0], FT_LOAD_RENDER);

        FT_GlyphSlot  slot = face->glyph;
        int xOff = slot->bitmap_left;
        int yOff = (face->size->metrics.ascender >> 6) - slot->bitmap_top;
        ConvertSlotToBuf32(&slot->bitmap, (INT32*)gp_TextBuf, ASCIISIZE * 48, cx + xOff, (yOff));
    }
    SDL_Surface* asciiSurf;
    if(TextBufSize == 8)
    asciiSurf = SDL_CreateRGBSurfaceWithFormatFrom(gp_TextBuf, ASCIISIZE * 48, ASCIISIZE,
        8, ASCIISIZE * 48, SDL_PIXELFORMAT_RGB332);
    else
        asciiSurf = SDL_CreateRGBSurfaceWithFormatFrom(gp_TextBuf, ASCIISIZE * 48, ASCIISIZE,
            8, ASCIISIZE * 48 * sizeof(INT32), SDL_PIXELFORMAT_RGBA8888);
    //保存字符图
    //SDL_SaveBMP(asciiSurf, "ascII.bmp");
    gpASCIITexture = SDL_CreateTextureFromSurface(gpSdlRender, asciiSurf);
    SDL_FreeSurface(asciiSurf);

    //清理
    delete[] gp_TextBuf;
    fclose(fp);
    //
    FT_Done_Face(face);
    FT_Done_FreeType(library);

    return 0;
}

//目标为8byte
//输入:源字形结构,目标缓存,目标一行字节,目标行位移,目标列位移
//返回:0成功,其他失败
int GL_Render::ConvertSlotToBuf(FT_Bitmap* srcSlot, GLubyte* dstBuf,int dstPitch, int xPos, int yPos)
{
    if (srcSlot == NULL || dstBuf == NULL)
        return -1;
    if (xPos + srcSlot->width > dstPitch)
        return -1;
    for (int y = 0; y < srcSlot->rows; y++)
    {
        GLubyte* dstP = dstBuf + (static_cast<__int64>(yPos) + y) * dstPitch + xPos;
        GLubyte* srcP = srcSlot->buffer + static_cast<__int64>(y) * srcSlot->pitch;
        for (int x = 0; x < srcSlot->width; x++)
        {
            if (srcSlot->pixel_mode == 1)
            {
                //源为1byte
                dstP[x] = ((1 << (7 - (x & 7))) & srcP[(x >> 3)]) ? 255 : 0;
            }
            else
            {
                //源为8byte
                dstP[x] = srcP[x];
                //dstP[x] = srcP[x] ? 255 : 0;
            }
        }
    }
    return 0;
}

//目标为32位缓存
//输入:源字形结构,目标缓存,目标一行字节,目标行位移,目标列位移
//返回:0成功,其他失败
int GL_Render::ConvertSlotToBuf32(FT_Bitmap* srcSlot, INT32* dstBuf, int dstPitch, int xPos, int yPos)
{
    if (srcSlot == NULL || dstBuf == NULL)
        return -1;
    if (xPos + srcSlot->width > dstPitch)
        return -1;
    for (int y = 0; y < srcSlot->rows; y++)
    {
        INT32 * dstP = dstBuf + (static_cast<__int64>(yPos) + y) * dstPitch + xPos;
        GLubyte* srcP = srcSlot->buffer + static_cast<__int64>(y) * srcSlot->pitch;
        for (int x = 0; x < srcSlot->width; x++)
        {
            if (srcSlot->pixel_mode == 1)
            {
                //源为1byte
                dstP[x] = ((1 << (7 - (x & 7))) & srcP[(x >> 3)]) ? 0xffffffff : 0;
            }
            else
            {
                //源为8byte
                dstP[x] = srcP[x] ? 0xffffff | srcP[x] << 24 : 0;
            }
        }
    }
    return 0;
}


SIZE GL_Render::DrawWideText(LPCWSTR lpszTextR, _POS pos, SDL_Color bColor, BOOL fShadow, BOOL fUpdate, int size)
{
    //功能:在屏幕上打印宽字符串
    //返回:占用屏幕大小
    int cx = POS_X(pos) * PictureRatio;
    int cy = POS_Y(pos) * PictureRatio;

    int len = lstrlenW(lpszTextR);
    size *= PictureRatio;
    SIZE wsz = { 0,size };
    SIZE zsz = { size,size };
    SDL_Rect srcRect;
    SDL_Rect dstRect;
    SDL_Texture* srcTexture;
    for (int n = 0; n < len; n++)
    {
        if (isascii(lpszTextR[n]))
        {
            if (lpszTextR[n] < 32)
                continue;
            zsz = { size / 2,size };
            srcTexture = gpASCIITexture;
            srcRect = { (lpszTextR[n] - 32) * ASCIISIZE / 2,0,ASCIISIZE / 2,ASCIISIZE };
            dstRect = { cx,cy,zsz.cx,zsz.cy };
            cx += zsz.cx;
            wsz.cx += zsz.cx;
        }
        else
        {
            zsz = { size,size };
            srcTexture = gpFontTexture;
            WCHAR s = lpszTextR[n];
            map<WCHAR, INT> ::iterator iter;
            iter = FontMap.find(s);
            if (iter == FontMap.end())
                //no find
                continue;
            int off = iter->second;
            srcRect = { (off & 63) * HZSIZE,(off >> 6) * HZSIZE,HZSIZE,HZSIZE };
            dstRect = { cx,cy,zsz.cx,zsz.cy };
            cx += zsz.cx;
            wsz.cx += zsz.cx;
        }
        if (fShadow)
        {
            SDL_Color mColor = { 2,2,2,0 };
            SDL_Rect mdstRect = dstRect;
            mdstRect.x++;
            RenderBlendCopy(gpRenderTexture, srcTexture, nullptr, 255, 6,
                &mColor, &mdstRect, &srcRect);
            mdstRect.y++;
            RenderBlendCopy(gpRenderTexture, srcTexture, nullptr, 255, 6,
                &mColor, &mdstRect, &srcRect);
        }
        if (bColor.r == 0 && bColor.g == 0 && bColor.b == 0)
            bColor = { 4,4,4,128 };
        RenderBlendCopy(gpRenderTexture, srcTexture, nullptr, 255, 6,
            &bColor, &dstRect, &srcRect);
    }
    if (fUpdate)
    {
        RenderPresent(gpRenderTexture);
    }
    return wsz;
}


 二、GLSL应用

先说片元着色器,也称为片段着色器,片元着色器所执行的是渲染过程中的最后阶段,目标是确定最终计算体现的每个具体像素的颜色,这个过程是并发进行的,也就是说GPU一次要并行计算很多像素,并且,计算像素的顺序是不确定的,所以整个计算过程中,各个原始纹理是只读的。OpenGL最新版本虽然可以支持纹理的修改,但这是有条件的。对纹理的读取(采样)点可能包括多个像素,这决定于采样策略。也可能不是原始的纹理,而是多级渐远而形成的次生纹理,在缺省的情况下,原始纹理像素和目标像素之间不存在一一对应的关系。(显卡本身是应该支持原始纹理读取的,在较新的OpenGL版本中也已经支持原始纹理像素的读取。因为除了图像处理,显卡还支持并行计算,并且总的计算能力是CUP的很多倍。)之所以这样处理,也是实际需求,因为如果对原始纹理一个像素一个像素的读取和处理,给程序的开发带来很大工作量,所以OpenGL对坐标进行了归一化预处理。GLSL着色器程序,每次只处理一个目标像素点,每个目标像素点的颜色,不会影响其他目标像素点颜色。

片元着色器可以对各种信息来源的数据,包括外部输入的,顶点着色器传入,纹理的对应位置信息,以及相关颜色信息,深度信息,这意味着片元着色器可以做许多事,实现很多功能。简单的如纹理混合,颜色过滤,透明,混合、颜色映射反转等等,这些功能在片元着色器中,往往只用几条简单命令就可以实现,这些简单操作都有一个共同的特征,就是只对缺省的固定位置进行纹理采样,这些操作虽然可以用固定管线方式实现,但GLSL实现起来更加简便和易于理解,这也是OpenGL舍弃固定管线操作的原因,除了以上特点之外,GLSL可以对纹理的任意位置进行采样,并进行计算、着色,从而实现特定的渲染目标。如实现屏幕水平分屏操作,

//正常纹理取样操作
//v_tex 纹理 sTexCoord 顶点着色器传递过来的 纹理UV坐标 outColor 输出颜色
outColor = texture(v_tex,sTexCoord);

//水平二分屏
outColor = texture(v_tex,vec2(fract(sTexCoord.x * 2.0) ,sTexCoord.y));

//再以0.4,0.6 为圆心 0.2为半径,0.02宽度 画一个红色的圆
float len = distance(sTexCoord, vec2(0.4,0.6));\n\
if(len <= 0.22 && len >=0.2)\n\
    outColor = vec4(1.0,0.0,0.0,1.0);\n\

结果如下:

 当然,这个圆有点扁,这是界面的长和宽比例问题,解决起来不难,图片的长宽比为1.6

UV坐标为x,y 距离就是 pow( pow(x - 0.4,2 ) * 1.6+ pow((x - 0.6) ,2),0.5);

 当然,也可以将红圈做成半透明的

    float len = pow( pow((sTexCoord.x-0.4)*1.6 ,2 ) + pow(sTexCoord.y-0.6,2),0.5);\n\
    if(len <= 0.22 && len >=0.2)\n\
        outColor = outColor * 0.5 + vec4(1.0,0.0,0.0,1.0)* 0.5;\n\

 有关图片透明,可以有很多方法,

一是使用两个纹理,在片元着色器中采样混合,这种方法有一定局限性,除全局混合外,局部混合的实现很麻烦,因为无论纹理大小,OpenGL着色器在处理时要对纹理坐标归一化,如采用采样计算的方法也可以实现局部混合透明化,但必须输入参数,而向GLSL输入数据是一件很麻烦的事,采样点的计算也很麻烦,尽管GLSL着色器程序长度几乎没有限制,但着色器的编制调试还是比c++困难得多,尤其是单步调试和排错手段缺失,关键字补全和检查也没有其他编程语言方便,中间结果检查还须借助专用工具使用CPU模拟实现,而且这类工具通用性又极差,单从编制效率来说,能从外部实现的,还是应该从外部实现。

二是外部实现,实现的方法也很简单,先在帧缓存中生成背景,也就是先渲染背景图片,之后再渲染前景图片,要使前景图片透明,必须满足两个条件,一个是要开启OpenGL的Alpha透明,一个是要设置前景的Alpha通道值。如果前景图片自身格式是RGBA或其他包含Alpha通道的颜色格式,并且已经设置好了,可以直接使用,否则,需要在片元着色器中设置。方法很简单,

//开启OpenGL的Alpha混合
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);//设置融合函数
    
//渲染,扇型四边型, 从第一个顶点开始 四个顶点
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

//关闭混合
    glDisable(GL_BLEND);

//在片元着色器中对输出颜色进行设置 Alpha 透明度,值为(0.0 -- 1.0)
outColor.a = Alpha;\n\

顶点着色器,和片元着色器任务不同,如果片元着色器主要任务是通过采样对颜色进行计算,顶点着色器重点就是对位置进行计算,位置包括大小、移动、旋转等各种拓扑变形,顶点着色器通常还能接收顶点颜色,光照等,使之对最终渲染结果产生影响,但对于这部分我还没有研究透,通常的SDL应用对这方面涉及的也比较少,在这里就不进行介绍了,重点要说的是对位置的变换和计算。提到图形的变换,离不开矩阵运算,使用矩阵为以大大地减轻编程负担,常用变换,往往一个矩阵就能搞定以下就分别介绍

单位矩阵,单位矩阵是对角线为1,其他单元为0的矩阵,在运算上,用同维度的单位矩阵乘同维数组,结果不变。GLSL对位置的描述使用的是4维数组,{x,y,z,w} x, y, z分别代表x ,y , z 轴位置,w 代表观测者距离。在GLSL着色器中,用vec2,vec3,vec4 分别代表 2维,3维,4维数组,用mat2,mat3,mat4 代表2元,3元,4元矩阵。mat2(1),mat3(1),mat4(1),分别代表相应的单位矩阵。vec4 x = mat4(1) * x。矩阵各位置的几何意义比较费解,但只要记住几个特殊位置就够了。

4维 矩阵1  对角线

\begin{bmatrix} x,0,0,0 & & & \\ 0,y,0,0 & & & \\ 0,0,z,0 & & & \\ 0,0,0,w \end{bmatrix}       

x,代表x轴宽度 ,y代表y轴宽度,z,代表z 轴宽度 w 代表观察者 对于2D图形,x 增加宽度增加,y会引发高度的变化,z 不起作用,w 加大图形会变远矩形会变小,反之变大。

4维 矩阵2 位移矩阵 

\begin{bmatrix}1,0,0,x & & & \\ 0,1,0,y & & & \\ 0,0,1,z & & & \\ 0,0,0,w & & & \end{bmatrix}  

对 2D应用 x 代表 x轴移动,y 代表 y轴移动 z 不起作用  w 作用同上

4维 矩阵 观察者位置矩阵

\begin{bmatrix} 1,0,0,0 & & & \\ 0,1,0,0 & & & \\ 0,0,1,0 & & & \\ x,y,z,w \end{bmatrix}

x,y,z分别代表观察者位置分别延相应的轴移动移动 ,w 作用不变

还有旋转矩阵,在这不啰嗦了,网上有大量介绍,讲得比我好,这里主要讲矩阵的用法,还是使用自建结构 SDL_fColor,主要是这个结构和 GLSL中四维数组的大小正好相同,外部引用{r,g,b,a} 与与GLSL中的vec4 相同,

为了控制图片显示的大小引入 SDL_fColor g_zoom = {x,y,z,w};   其中 x,y 分别控制左右和上下位移,z 无作用,w 控制放大和缩小。

为了控制图片旋转,引入 SDL_fColor g_roll = {x,y,z,w};其中 x,y,z 分别控制 x,y,z 轴的旋转角度。w 控制放大与缩小

将顶点着色器代码修改成

//顶点着色器
const static GLchar* const vertexShader =
"#version 130\n\
in vec2 position;\n\
in vec2 TexCoord;\n\
uniform vec4 v_data[20];\n\
out vec2 sTexCoord;\n\
void main()\n\
{\n\
    vec4 zoom  = v_data[4];\n\
    vec4 roll  = v_data[5];\n\
    gl_Position = vec4(position, 0.0, 1.0) ;\n\
    if(zoom.w > 0.0)//移动缩放\n\
    {\n\
        mat4 m_zoom = mat4(1.0);\n\
        m_zoom[3] = zoom;\n\
        gl_Position = m_zoom * gl_Position;\n\
    }\n\
    if(roll.w >1.0)//旋转\n\
    {\n\
        float s,c;\n\
        mat4 m_roll = mat4(1.0);//X轴\n\
        m_roll[3][3] = roll.w;\n\
        m_roll[1][1] = m_roll[2][2] = cos(roll.x);\n\
        m_roll[1][2] = sin(roll.x);\n\
        m_roll[2][1] = -sin(roll.x);\n\
        gl_Position = m_roll * gl_Position;\n\
        m_roll = mat4(1.0);//Y轴\n\
        m_roll[0][0] = m_roll[2][2] = cos(roll.y);\n\
        m_roll[0][2] = sin(roll.y);\n\
        m_roll[2][0] = -sin(roll.y);\n\
        gl_Position = m_roll * gl_Position;\n\
        m_roll = mat4(1.0);//Z轴\n\
        m_roll[0][0] = m_roll[1][1] = cos(roll.z);\n\
        m_roll[0][1] = sin(roll.z);\n\
        m_roll[1][0] = -sin(roll.z);\n\
        gl_Position = m_roll * gl_Position;\n\
    }\n\
    sTexCoord = TexCoord ;\n\
}";

其中 v_data[20] 与片元着色器共用一组参数

最后贴上一组效果图片

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值