OpenGL ES 2.0 完全入门

一看就懂的OpenGL ES教程——图形渲染管线的那些事 学习OpenGL首当其冲的就是要清楚它的工作机制,它不像平时做Android应用开发那样,只要几个api理解就差不多可以做一个功能开发了,OpenGL需要的是大局观,我们要像一位将军一样,在战役中统揽大局,把整个处理流程搞清楚,才能指定好战略,才能“拿下”OpenGL,而这里的“大局”,指的就是。图中每个方格表示一个片段,片段上的数值表示当前片段的深度,R则表示深度无限,加号表示2个图形叠加一起,则由下面部分的图可知,当2个图形叠加在一起的时候,同一个位置的片段总是显示深度较小的那一个。 阅读详情

1. 基本概念

在 OpenGL 的世界里,我们只能画点、线、三角形,复杂的图形都是由三角形构成的。

在 OpenGL 里有两个最基本的概念:Vertex 和 Fragment。一切图形都从 Vertix 开始,Vertix 序列围成了一个图形。那什么是 Fragment 呢?为此我们需要了解一下光栅化(Rasterization):光栅化是把点、线、三角形映射到屏幕上的像素点的过程(每个映射区域叫一个 Fragment),也就是生成 Fragment 的过程。通常一个 Fragment 对应于屏幕上的一个像素,但高分辨率的屏幕可能会用多个像素点映射到一个 Fragment,以减少 GPU 的工作。

        接下来介绍 Shader(着色器程序):Shader 用来描述如何绘制(渲染),GLSL 是 OpenGL 的编程语言,全称 OpenGL Shader Language,它的语法类似于 C 语言。OpenGL 渲染需要两种 Shader:Vertex Shader 和 Fragment Shader。

        每个 Vertex 都会执行一遍 Vertex Shader,以确定 Vertex 的最终位置,其 main 函数中必须设置 gl_Position 全局变量,它将作为该 Vertex 的最终位置,进而把 Vertex 组合(assemble)成点、线、三角形。光栅化之后,每个 Fragment 都会执行一次 Fragment Shader,以确定每个 Fragment 的颜色,其 main 函数中必须设置 gl_FragColor 全局变量,它将作为该 Fragment 的最终颜色。

 2. 坐标系

        弄 清楚坐标系很重要,不然会找不着东南西北。下面这张图展示了 OpenGL 通常的处理流程中各个环节的坐标系,以及坐标系之间的转换操作

        

        下面对图中的几个概念作简单说明:

  • Local space:我们为每个物体建好模型的时候,它们的坐标就是 Local space 坐标;
  • World space:当我们要绘制多个物体时,如果直接使用 Local space 的坐标(把所有物体的原点放在一起),那它们很可能会发生重叠,因此我们需要把它们进行合理的移动、排布,最终各自的坐标就是 World space 的坐标了;
  • Model matrix:把 Local space 坐标转换到 World space 坐标所使用的变换矩阵,它是针对每个物体做不同的变换;
  • View space:通常也叫 Camera space 或者 Eye space,是从观察者(也就是我们自己)所在的位置出发,所看到的空间;
  • View matrix:把 World space 坐标转换到 View space 坐标所使用的变换矩阵,它相当于是在移动相机位置,实际上是反方向移动整个场景(所有物体);
  • Clip space:OpenGL 只会渲染坐标值范围在 [-1, 1] 的内容,超出这个范围的内容都会被裁剪掉,这个范围的空间就叫 Clip space,Clip space 的坐标系也叫 normalized device coordinate(NDC);
  • View space:通常也叫 Camera space 或者 Eye space,是从观察者(也就是我们自己)所在的位置出发,所看到的空间;
  • View matrix:把 World space 坐标转换到 View space 坐标所使用的变换矩阵,它相当于是在移动相机位置,实际上是反方向移动整个场景(所有物体);
  • Clip space:OpenGL 只会渲染坐标值范围在 [-1, 1] 的内容,超出这个范围的内容都会被裁剪掉,这个范围的空间就叫 Clip space,Clip space 的坐标系也叫 normalized device coordinate(NDC);
  • Projection matrix:把 View space 坐标转换到 Clip space 坐标所使用的变换矩阵,它会指定一个可见的范围,只有这个范围内的点才会转换到 NDC 中,而这个范围被称作视锥(frustum);projection matrix 有三种创建方式:正投影(Orthographic projection),透视投影(Perspective projection),以及 3D 投影(3D projection);前两种比较常用。
  • Screen space:屏幕上的空间,glViewport 调用指定的区域;
  • Viewport transform:这一步是 Open GL 自动完成的,把 Clip space 坐标转换到 Screen space 坐标;

 下面对图中的几个概念作简单:

        

           LearnOpenGL - Coordinate Systems更多关于坐标系的内容,可以阅读:LearnOpenGL - Coordinate Systems      

3. Hello world

        先看一下最简单的完整例子(下文有详细分析,完整代码可以在 GitHub 获取),绘制一个三角形,效果如图一

public class MainActivity extends AppCompatActivity {

    private GLSurfaceView mGLSurfaceView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (!Utils.supportGlEs20(this)) {
            Toast.makeText(this, "GLES 2.0 not supported!", Toast.LENGTH_LONG).show();
            finish();
            return;
        }

        mGLSurfaceView = (GLSurfaceView) findViewById(R.id.surface);

        mGLSurfaceView.setEGLContextClientVersion(2);
        mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
        mGLSurfaceView.setRenderer(new MyRenderer());
        mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
    }

    @Override
    protected void onPause() {
        super.onPause();
        mGLSurfaceView.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mGLSurfaceView.onResume();
    }

    private static class MyRenderer implements GLSurfaceView.Renderer {

        private static final String VERTEX_SHADER =
                "attribute vec4 vPosition;\n"
                + "void main() {\n"
                + "  gl_Position = vPosition;\n"
                + "}";
        private static final String FRAGMENT_SHADER =
                "precision mediump float;\n"
                + "void main() {\n"
                + "  gl_FragColor = vec4(0.5, 0, 0, 1);\n"
                + "}";
        private static final float[] VERTEX = {   // in counterclockwise order:
                0, 1, 0,  // top
                -0.5f, -1, 0,  // bottom left
                1, -1, 0,  // bottom right
        };

        private final FloatBuffer mVertexBuffer;

        private int mProgram;
        private int mPositionHandle;

        MyRenderer() {
            mVertexBuffer = ByteBuffer.allocateDirect(VERTEX.length * 4)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer()
                    .put(VERTEX);
            mVertexBuffer.position(0);
        }

        static int loadShader(int type, String shaderCode) {
            int shader = GLES20.glCreateShader(type);
            GLES20.glShaderSource(shader, shaderCode);
            GLES20.glCompileShader(shader);
            return shader;
        }

        @Override
        public void onSurfaceCreated(GL10 unused, EGLConfig config) {
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

            mProgram = GLES20.glCreateProgram();
            int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
            int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
            GLES20.glAttachShader(mProgram, vertexShader);
            GLES20.glAttachShader(mProgram, fragmentShader);
            GLES20.glLinkProgram(mProgram);

            GLES20.glUseProgram(mProgram);

            mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

            GLES20.glEnableVertexAttribArray(mPositionHandle);
            GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false,
                    12, mVertexBuffer);
        }

        @Override
        public void onSurfaceChanged(GL10 unused, int width, int height) {
            GLES20.glViewport(0, 0, width, height);
        }

        @Override
        public void onDrawFrame(GL10 unused) {
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

            GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
        }
    }
}

 

 3.1. set up

           首先我们需要一个 GLSurfaceView,它是让我们渲染的“画布”。然后我们需要一个 GLSurfaceView.Renderer,它将实现我们的渲染逻辑。此外我们还将设置 GL ES 版本,并将 GLSurfaceView 和 Renderer 连接起来:

mGLSurfaceView.setEGLContextClientVersion(2);
mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
mGLSurfaceView.setRenderer(new MyRenderer());
mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);

        RenderMode 有两种,RENDERMODE_WHEN_DIRTY 和 RENDERMODE_CONTINUOUSLY,前者是懒惰渲染,需要手动调用 glSurfaceView.requestRender() 才会进行更新,而后者则是不停渲染。 

 3.2. GLSurfaceView.Renderer

         Renderer 包含三个接口:

public interface Renderer {
    void onSurfaceCreated(GL10 gl, EGLConfig config);
    void onSurfaceChanged(GL10 gl, int width, int height);
    void onDrawFrame(GL10 gl);
}

   onSurfaceCreated 在 surface 创建时被回调,通常用于进行初始化工作,只会被回调一次;onSurfaceChanged 在每次 surface 尺寸变化时被回调,注意,第一次得知 surface 的尺寸时也会回调;onDrawFrame 则在绘制每一帧的时候回调。 

  3.3. GLSL 程序 

        和普通的 view 利用 canvas 来绘制不一样,OpenGL 需要加载 GLSL 程序,让 GPU 进行绘制。所以我们需要定义 shader 代码,并在初始化时(也就是 onSurfaceCreated 回调中)加载:

@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

    mProgram = GLES20.glCreateProgram();
    int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
    int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
    GLES20.glAttachShader(mProgram, vertexShader);
    GLES20.glAttachShader(mProgram, fragmentShader);
    GLES20.glLinkProgram(mProgram);

    GLES20.glUseProgram(mProgram);

    mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

    GLES20.glEnableVertexAttribArray(mPositionHandle);
    GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false,
            12, mVertexBuffer);
}

static int loadShader(int type, String shaderCode) {
    int shader = GLES20.glCreateShader(type);
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);
    return shader;
}

          GLSL 的语法并不是本文的主要内容,这里就不深入展开了。

  • 创建 GLSL 程序:glCreateProgram
  • 加载 shader 代码:glShaderSource 和 glCompileShader
  • attatch shader 代码:glAttachShader
  • 链接 GLSL 程序:glLinkProgram
  • 使用 GLSL 程序:glUseProgram
  • 获取 shader 代码中的变量索引:glGetAttribLocation
  • 启用 vertex:glEnableVertexAttribArray
  • 绑定 vertex 坐标值:glVertexAttribPointer

          需要指出的是,我们的 Java 代码需要获取 shader 代码中定义的变量索引,用于在后面的绘制代码中进行赋值,变量索引在 GLSL 程序的生命周期内(链接之后和销毁之前),都是固定的,只需要获取一次。

 3.4. 设置 Screen space 的大小

        我们可以利用 glViewport 设置 Screen space 的大小,通常在 onSurfaceChanged 中调用:

@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
    GLES20.glViewport(0, 0, width, height);
}

3.5. 绘制 

        我们在 onDrawFrame 回调中执行绘制操作,绘制的过程其实就是为 shader 代码变量赋值,并调用绘制命令的过程:

@Override
public void onDrawFrame(GL10 unused) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}

        由于顶点坐标已经绑定过了,所以这里无需进行变量赋值,直接调用绘制指令即可。我们可以通过 GLES20.glDrawArrays 或者 GLES20.glDrawElements 开始绘制。注意,执行完毕之后,GPU 就在显存中处理好帧数据了,但此时并没有更新到 surface 上,是 GLSurfaceView 会在调用 renderer.onDrawFrame 之后,调用 eglSwapBuffers,来把显存的帧数据更新到 surface 上的。

         这里我们绘制的是一个三角形,OpenGL 坐标原点在屏幕中心,三个顶点分别是:

  • (0, 1, 0),位于屏幕顶部中心点;
  • (-0.5f, -1, 0),位于屏幕底部四分之一点;
  • (1, -1, 0),位于屏幕右下角;

         它们的 z 轴坐标都是 0,所以也就是上面图一的效果了。       

        #4. 投影变换 

        你肯定已经注意到,OpenGL 坐标系和安卓手机坐标系不是线性对应的,因为手机的宽高比几乎都不是 1。因此我们绘制的形状是变形的,怎么解决这个问题呢?答案是投影变换(projection)。前面我们已经知道,投影变换用于把 View space 的坐标转换为 Clip space 的坐标,在这个转换过程中,它还能顺带处理宽高比的问题。

        使用较多的是正投影和透视投影,这里我们使用透视投影:Matrix.perspectiveM。通常坐标系的变换都是对顶点坐标进行矩阵左乘运算,因此我们需要修改我们的 vertex shader 代码:

private static final String VERTEX_SHADER = 
        "attribute vec4 vPosition;\n"
        + "uniform mat4 uMVPMatrix;\n"
        + "void main() {\n"
        + "  gl_Position = uMVPMatrix * vPosition;\n"
        + "}";

安卓 OpenGL ES 2.0 完全入门(一):基本概念和 hello world - Piasy的博客 | Piasy Blog 

OpenGL ES入门指南(一) 想学OpenGL?想在手机端上实现酷炫效果?那还不快来了解OpenGL ES 阅读详情

相关推荐

OpenGL ES 2.0 for Android教程(一)

本文是一系列OpenGL ES 2.0 for Android教程,基于Kotlin与一点点Compose来创建代码示例。通过本系列教程你可以从零开始一步步学习OpenGL ES。

重来的博客 7477

OpenGL ES2.0基础

初级学习OpenGL ES2.0的课程,从无到有,从进本的函数讲起,每一课时都附带一个例子程序。深入浅出的讲解可编程管线技术,令人费解的文理,以及混合技术,各种优化技术:顶点缓冲区,索引缓冲区,帧缓冲区,介绍精灵的使用,并使用shader制作粒子特效。

OpenGL ES 2.0 入门(持续更新)

请尊重原创,转载请注明出处:http://blog.csdn.net/mabeijianxi/article/details/79186086 发现公司不管是游戏业务还是视频业务无不与 Open GL 息息相关,于是每周抽了点时间操练下,万一哪天被点将点中了也是不虚的,下面是笔者的一些学习笔记。 一、Open GL 2.0 的渲染管道 Created with Raphaël 2.1...

5381

安卓 OpenGL ES 2.0 完全入门(一):基本概念和 hello world

本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2016/06/07/Open-gl-es-android-2-part-1/做安卓开发满打满算也有 3 年了,OpenGL 这块之前完全没有涉及过,这两周一直在整理安卓相机预览、用 GPUImage 进行美颜处理以及美颜后的数据传输这块内容,结果 GP...

sinat_41794875的博客 1011

OpenGL ES 入门指南

OpenGL ES (OpenGL for Embedded Systems) 是OpenGL的子集,专为移动设备和嵌入式系统设计。它是智能手机、平板电脑、游戏主机等设备上2D/3D图形渲染的标准API。

深之JohnChen的专栏 2502

OpenGL ES 2.0 编程指南 学习

OpenGL ES 2.0 Programme Guide <br />基础内容学习小结1. OverView <br />ES2.0绘图管线示意图:<br /><br /><br />VertexArray : 绘图场景中所有的物体空间位置由点坐标数组构成。<br />VertexShader: 对每个点的位置进行变换,计算光照方程式生成每点的颜色,生成或者变换纹理坐标。基本结构示意图:<br /><br />Attributes  ---- 采用点向量数组形式传入的每个点的数据。<br />Unifor

beelike的专栏 2万+

OpenGL ES 2.0 完全入门(二):矩形、图片、读取显存等

1. 绘制矩形 上篇中有提到,三角形是基本形状,利用三角形我们可以“拼出”其他的任何形状,例如矩形。 绘制两个三角形时,我们可以指定 6 个顶点的坐标,但实际上只有 4 个不同的点,这样有点浪费,OpenGL 支持用另一种方式完成绘制:用一个数组保存顶点数据,用另一个数组保存顶点的绘制顺序: private static final float[] VERTEX = { // in counterclockwise order: 1, 1, 0, ...

EveryDayOneHour的博客 1436

Win10下搭建OPenGL ES2.0的开发环境

一,所需要的软件和代码: (1)VS2015 (2)Opengl ES2.0模拟器 (3)《OpenGL ES2.0 Programming guide》的例子源码 下载地址:资料下载   二,安装 1.模拟器运行ESEmulator.2009-04-28-v1.4.APRIL_2009_RELEASE.msi,按照提示安装。       在模拟器的安装目录下,找到bin目录下的文...

默茉的博客 3736

安卓 OpenGL ES 2.0 完全入门(二):矩形、图片、读取显存等

https://blog.piasy.com/2016/06/14/Open-gl-es-android-2-part-2/   本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2016/06/14/Open-gl-es-android-2-part-2/ 在安卓 OpenGL ES 2.0 完...

u010029439的博客 676

DSMA时序修复避坑指南:当Setup和Hold互卡时,除了调Margin还能做什么?

本文深入探讨DSMA时序修复中的Setup与Hold互锁问题,提供7种高阶策略,包括优先级判断、高级参数组合、单元类型智能替换等,帮助工程师突破时序收敛瓶颈。特别针对fix_eco_timing命令的隐藏参数和物理协同优化进行详细解析,助力芯片设计实现时序收敛。

weixin_30162121的博客 294

基于STM32的Modbus TCP多主站数据监测

Modbus TCP通信协议在STM32上实现,STM32作为服务器采集内部传感器数据,并通过ADC进行转换,存入寄存器。两个PC机作为客户端访问服务器,服务器通过Socket通信实现监听连接,将数据发送给客户端。

hyp12347的博客 4291

OpenGL ES入门系列 一

Android UI OpenGL ES入门系列 一参考 OpenGL ES是OpenGL针对嵌入式设备精简优化版本 参考 学OpenGL的一点心得体会 wiki

初心一点的专栏 229

300Kg载重履带式无人车底盘定制开发技术详解

通过精准的需求分析、科学的方案设计、严格的研发与测试以及完善的交付与培训流程,可以打造出符合客户需求的优质产品。300Kg载重履带式无人车底盘的定制开发,旨在打造一款承载能力强、行驶稳定、适应复杂地形的无人运输平台。根据应用场景选择合适的动力源,并进行性能优化,确保无人车在载重情况下仍能保持稳定的行驶速度和动力输出。根据需求分析结果,设计无人车底盘的总体方案,包括动力系统选择、结构设计、负载管理策略等。进行详细的研发工作,包括零部件的选型与采购、样机的制作与调试等。

专注多旋翼、复合翼无人机技术研究,从事低空经济行业。 958

安卓 OpenGL ES 2.0 完全入门(三):2D 纹理的裁剪、翻转、旋转、缩放

https://blog.piasy.com/2017/10/06/Open-gl-es-android-2-part-3/   本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2017/10/06/Open-gl-es-android-2-part-3/ 我在去年六月份学习了 OpenGL 的...

u010029439的博客 2221

python 识别滑块验证码

博客来源于: https://www.cnblogs.com/wyh0923/p/16528354.html # -*- coding: utf-8 -*- # https://gov.pkulaw.cn/ 1. import io 2. import json 3. from pathlib import Path 4. from PIL import Image 5. import bas...

2102

开关电源拓扑结构选型指南:从Buck到Full-Bridge的实战解析

本文为硬件工程师提供了开关电源拓扑结构的实战选型指南。文章系统解析了从基础的Buck、Boost、Buck-Boost,到隔离型的Flyback、Forward,再到中高功率的半桥、全桥以及高效率的LLC谐振拓扑的适用场景与核心特点。通过对比分析各拓扑的优缺点,并结合输入输出电压、功率等级、成本与效率等关键参数,给出了清晰的选型决策路径,帮助工程师根据具体项目需求选择最合适的电源“骨架”。

weixin_29306261的博客 106

OpenGL ES入门介绍

OpenGL(Open Graphics Library)是一个跨平台图像程序接口,用于二维和三位图片的渲染。OpenGL ES(Open Graphic Library Embedded Systems)是OpenGL的一个子集,用于手机和嵌入式设备在android中对OpenGL ES的支持如下1. OpenGL ES 2.0 支持android2.2以后的版本2. OpenGL ES 3.0 支持android4.3以后的版本。

WolfKingzyh的博客 1771

Android RIL(Radio Interface Layer)全面概述和知识要点(3万字长文)

在 Android 系统这个庞大而复杂的体系里,RIL(Radio Interface Layer,无线接口层)绝对是个关键角色。简单来讲,它就像一座桥梁,一头连着手机基带,另一头接着上层应用 ,在应用程序和调制解调器(Modem)之间搭建起了信息交流的通道。想象一下,咱们在手机上点击拨号按钮,或者编辑一条短信发送出去,这些操作产生的请求并不会直接到达手机的基带处理器。而是先由 RIL 接手,它把这些来自上层应用程序的请求,像拨号、发送短信这类,巧妙地转换成能够和基带处理器顺畅交互的命令。

大模型大数据攻城狮的专栏 1595

OpenGL ES3.0

另外,为了防止形变效果的跳变,我们还需要平滑函数满足在边界处输出值为 0 (或者接近于 0 ),表示此边界为是否发生形变的临界线。

SmallWalnutBG的博客 2204

virtuoso如何导出def?

在DEF Out界面,填写def name,library name以及cell name ,cell view既layout,把Output "Floating Shapes" into SPECIAL NETS Section勾上,OK即可。2)在要导出的layout界面选中PAD和prBoundary(shift+左键),快捷键C copy到空cell里,这时需要调整copy过来的object坐标,让两边prBoundary的坐标相同。我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧?

m0_61544122的博客 2304

Python爬虫实战:全网政府招标信息采集与整合

本文详细介绍了如何使用Python构建一个完整的政府招标信息爬虫系统,涵盖了从数据采集、解析、存储到质量监控的全流程。多源采集:支持多个地方政府网站的同时采集智能解析:自动适应不同网站结构,智能提取关键信息异步高效:使用异步编程提高采集效率反爬应对:内置多种反爬虫应对策略数据质量:完善的数据清洗和质量监控机制易于扩展:模块化设计,便于添加新的数据源。

2201_76125261的博客 1380
上一篇: YUV 数据格式完全解析
下一篇: OpenGL ES2.0 的三种变量类型(uniform,attribute和varying)
怕什么真理无穷
博客等级 码龄10年 415粉丝 195原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值