WebGL - 纹理映射

本文详细介绍了WebGL中的纹理映射,包括纹理映射的概念、四步映射过程、纹理坐标及其映射过程。在映射过程中,涉及到顶点着色器、片元着色器的配置,以及纹理坐标传递、纹理加载与配置等关键步骤,如图像Y轴反转、激活纹理单元和绑定纹理对象等。最后,文章还讨论了如何使用多张纹理贴图。
摘要由CSDN通过智能技术生成

1、纹理映射

纹理映射:纹理映射就是将一张图像映射大盘几何体表面上去,此时这样图片就称为纹理贴图;

纹理映射的作用,就是根据纹理图像,为之前光栅化后的每一个片元涂上合适的颜色,组成纹理图像的像素又被称为 纹素,每一个纹素的颜色都使用 RGB或者 RGBA格式编码

纹素可以简单的理解为一个像素

2、纹理映射四步

  • 1、准备纹理图像
  • 2、配置集合图形纹理映射方式
  • 3、加载和配置纹理图像
  • 4、从纹理中抽出纹素赋给片元

3、纹理坐标

纹理坐标:就是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色,webgl系统中的纹理坐标是 二维的;

纹理坐标用来确定纹理图像那部分覆盖到集合图形上,纹理坐标值与图像自身的尺寸无关

在这里插入图片描述

4、映射过程

纹理映射得过程需要顶点着色器和片元着色器二者得配合;

1、初始化着色器

如下着色器代码

顶点着色器

attribute vec4 a_Position;
attribute vec4 a_TexCoord;
varying vec2 v_TexCoord;
void main(){
    gl_Position = a_Position;
    v_TexCoord = a_TexCoord;
}

片元着色器

precision mediump float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main(){
    gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
2、设置纹理坐标

将纹理坐标传入顶点着色器,与将其他顶点数据(如颜色),传入顶点着色器得方法是一样的;

// 创建顶点坐标和纹理坐标
var verticesTexCoords = new Float32Array([
    // 顶点坐标, 纹理坐标
    -0.5,  0.5,  0.0, 1.0,
    -0.5, -0.5,  0.0, 0.0,
     0.5,  0.5,  1.0, 1.0,
     0.5, -0.5,  1.0, 0.0
]);
3、配置和加载纹理
/* 四、创建纹理对象并调用纹理绘制方法 */
function initTextures(gl, n) {

    // 创建纹理对象
    var texture = gl.createTexture();
    if (!texture) {
        console.log('创建纹理对象失败!');
        return false;
    }

    // 获取 u_Sampler 的存储位置
    var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
    if (!u_Sampler) {
        console.log('获取 uniform 变量 u_Sampler 失败!');
        return false;
    }

    // 创建 Image 对象
    var image = new Image();
    image.onload = function (ev) {
        loadTexture(gl, n, texture, u_Sampler, image);
    };

    image.src = '../resources/sky.JPG';

    return true;

}

上面的代码负责配置和加载纹理,首先 创建纹理gl.createTexture(),然后通过 gl.getUniformLocation()从片元着色器中获取 uniform变量 u_Sampler(取样器)的存储位置,用来接收纹理图像;

可以通过 gl.deleteTexture()删除一个纹理对象,注意:如果视图删除一个已经被删除的纹理对象,不会报错也不会产生影响

4、配置纹理参数
/* 五、设置纹理相关信息 */
function loadTexture(gl, n, texture, u_Sampler, image) {

    // 对纹理图形进行y轴反转
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

    // 开启0号纹理单元
    gl.activeTexture(gl.TEXTURE0);

    // 向target绑定纹理对象
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // 配置纹理参数
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

    // 配置纹理对象
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);

    // 将0号纹理单元传递给取样器变量
    gl.uniform1i(u_Sampler, 0);

    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);

}

以上代码对纹理进行了配置,下面是具体过程

1、图像Y轴反转

图像Y轴反转是比较重要的一步,gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1),因为webgl纹理坐标系统的u轴,的方向和一些png、jpg等格式图片的坐标系统的y轴方向式相反的,如果不进行图像y轴反转则映射之后看到的式向下倒着的图像;

如果你了解three.js这个webgl引擎的话,它的内部在你加载纹理贴图的时候就已经进行y轴反转了,可以在纹理贴图部分看下这个Texture对象;

.flipY : boolean,默认为true,翻转图像的Y轴以匹配WebGL纹理坐标

2、激活纹理单元

webgl通过一种称作 纹理单元的机制来同时使用多个纹理(在同一个几何体平面),每个纹理单元有一个单元编号,来管理一张纹理图像,即使只是用一张纹理贴图,也要为其指定一个纹理单元,默认情况下至少支持8个纹理单元;

gl.activeTexture(gl.TEXTURE0);//开启0号纹理单元
3、绑定纹理对象

与绑定缓冲区很类似,在对缓冲区进行操作之前也需要绑定缓冲区对象

webgl支持两种类型的纹理

  • gl.TEXTURE_2D 二维纹理
  • gl.TEXTURE_CUBE_MAP 立方体纹理
gl.bindTexture(gl.TEXTURE_2D,texture);//绑定纹理对象

实际上,在webgl中,你没法直接操作纹理对象,必须通过将纹理对象绑定到纹理单元上,然后通过操作纹理单元来操作纹理对象;

4、纹理对象参数

绑定之后还需要对纹理对象做更具体的配置;

// 配置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  • 第一个参数是纹理的类型;
  • 第二个是获取纹素颜色的方式;
  • 第三个是赋值给第二个参数的值;

参数二具体如下

  • 放大方法:gl.TEXTURE_MAG_FILTER,表示当纹理的绘制范围比纹理本身更大时,获取纹素颜色的方式;
  • 缩小方法:gl.TEXTURE_MIN_FILTER,表示当纹理的绘制范围比纹理本身小的时,获取纹素颜色的方式;
  • 水平填充方法:gl.TEXTURE_WRAP_S, 表示对纹理图像左侧或者右侧的区域进行填充;
  • 垂直填充方法:gl.TEXTURE_WRAP_T,表示如何对纹理图像上方和下方的区域进行填充;

设置方式

通过gl.texParameteri()来设置,上面参数的默认值如下,通常可以不需要调用gl.texParameteri()就可以使用默认值;

纹理参数描述默认值
gl.TEXTURE_MAG_FILTER纹理放大gl.LINEAR
gl.TEXTURE_MIN_FILTER纹理缩小gl.NEAREST_MIPMAP_LINEAR
gl.TEXTURE_WRAP_S纹理水平填充gl.REPEAT
gl.TEXTURE_WRAP_T纹理垂直填充gl.REPEAT

可赋值的常量值描述

1、可赋值给gl.TEXTURE_MAG_FILTERgl.TEXTURE_MIN_FILTER的常量如下

  • gl.NEAREST:使用原纹理上距离映射后像素中心最近的那个像素的颜色值,作为新像素的值;
  • gl.LINEAR:使用距离新像素中兴最近的四个像素的颜色值的加权平均值,作为新像素的值,与gl.NEAREST相比,该方法图像质量更好,但是会有较大的开销;

2、可赋值给gl.TEXTURE_WRAP_Sgl.TEXTURE_WRAP_T的常量如下

  • gl.REPEAT :平铺式重复纹理;
  • gl.MIRRORED_REPEAT :镜像对称式的重复纹理;
  • gl.CLAMP_TO_EDGE :使用纹理图像边缘值;
5、分配给纹理对象
// 分配纹理对象
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
  • 参数一:纹理类型;
  • 参数二:默认0
  • 参数三:图像内部格式;
  • 参数四:纹理数据的格式;
  • 参数五:纹理数据的类型;
  • 参数六:包含纹理图像的Image对象;

纹素数据的格式

格式描述
gl.RGB红、绿、蓝
gl.RGBA红、绿、蓝、透明度
gl.ALPHA(0.0, 0.0, 0.0, 透明度)
gl.LUMINACELLL1L:流明
gl.LUMINANCE_ALPHALLL、透明度

流明 :表示我们感知到的物体表面的亮度,通常使用物体表面红、绿、蓝颜色值的加权平均来计算流明;

然后,后面就是将纹理单元传递给取样器变量u_Sampler

// 将0号纹理单元传递给取样器变量
gl.uniform1i(u_Sampler, 0);
6、使用多福纹理

webgl可以同时处理多福纹理,因为可以同时使用多个纹理单元,两个纹理就需要两个纹理单元;

顶点着色器不用修改,因为两幅纹理图像所用的是一样的纹理坐标,只需要在片元着色器中再添加一个取样器变量即可uniform sampler2D u_Sampler;

着色器代码

precision mediump float;
uniform sampler2D u_Sampler;
uniform sampler2D u_Sampler1;//新增取样器变量
varying vec2 v_TexCoord;
void main(){
    vec4 color = texture2D(u_Sampler,v_TexCoord);
    vec4 color1 = texture2D(u_Sampler,v_TexCoord);
    gl_FragColor = color * color1;//这是颜色矢量的相乘得到新的矢量
}

下面的操作和加载一张时一样,只是需要等到两张纹理全部加载进来之后,在开始绘制;

纹理加载基础代码

/* 一、着色器部分 */
// 顶点着色器
var vertex_shader_source = '' +
    'attribute vec4 a_Position;' +
    'attribute vec2 a_TexCoord;' +
    'varying vec2 v_TexCoord;' +
    'void main() {' +
    '   gl_Position = a_Position;' +
    '   v_TexCoord = a_TexCoord;' +
    '}';

// 片元着色器
var fragment_shader_source = '' +
    'precision mediump float;' +
    'uniform sampler2D u_Sampler;' +
    'varying vec2 v_TexCoord;' +
    'void main(){' +
    '   gl_FragColor = texture2D(u_Sampler, v_TexCoord);' +
    '}';

/* 二、 初始化着色器,设置顶点信息*/
(function () {

    // 获取canvas对象
    var canvas = document.getElementById('webgl');

    // 获取webgl 上下文对象
    var gl = getWebGLContext(canvas);

    // 初始化着色器
    if (!initShaders(gl, vertex_shader_source, fragment_shader_source)) {
        console.log('初始化着色器失败!');
        return false;
    }

    // 设置顶点位置
    var n = initVertexBuffer(gl);
    if (n < 0) {
        console.log('顶点写入缓存失败!');
        return false;
    }

    // 指定清除颜色
    gl.clearColor(0.0, 0.5, 0.5, 1.0);

    // 配置纹理
    if (!initTextures(gl, n)){
        console.log('无法配置纹理!');
        return false;
    }

}());

/* 三、 设置顶点坐标和纹理坐标*/
function initVertexBuffer(gl) {

    // 创建顶点坐标和纹理坐标
    var verticesTexCoords = new Float32Array([
        // 顶点坐标, 纹理坐标
        -0.5,  0.5,  0.0, 1.0,
        -0.5, -0.5,  0.0, 0.0,
        0.5,  0.5,  1.0, 1.0,
        0.5, -0.5,  1.0, 0.0
    ]);

    var n = 4;

    // 创建缓冲区
    var vertexTexCoordBuffer = gl.createBuffer();
    if (!vertexTexCoordBuffer) {
        console.log('创建缓冲区失败!');
        return -1;
    }

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);

    var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;

    var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    if (a_Position < 0) {
        console.log('获取 attribute 变量 a_Position 失败!');
        return -1;
    }

    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
    gl.enableVertexAttribArray(a_Position);

    var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
    if (a_TexCoord < 0) {
        console.log('获取 attribute 变量 a_TexCoord 失败!');
        return -1;
    }

    gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
    gl.enableVertexAttribArray(a_TexCoord);

    return n;

}

/* 四、创建纹理对象并调用纹理绘制方法 */
function initTextures(gl, n) {

    // 创建纹理对象
    var texture = gl.createTexture();
    if (!texture) {
        console.log('创建纹理对象失败!');
        return false;
    }

    // 获取 u_Sampler 的存储位置
    var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
    if (!u_Sampler) {
        console.log('获取 uniform 变量 u_Sampler 失败!');
        return false;
    }

    // 创建 Image 对象
    var image = new Image();
    image.onload = function (ev) {
        loadTexture(gl, n, texture, u_Sampler, image);
    };

    image.src = '../resources/sky.JPG';

    return true;

}

/* 五、设置纹理相关信息 */
function loadTexture(gl, n, texture, u_Sampler, image) {

    // 对纹理图形进行y轴反转
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

    // 开启0号纹理单元
    gl.activeTexture(gl.TEXTURE0);

    // 向target绑定纹理对象
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // 配置纹理参数
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

    // 配置纹理对象
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);

    // 将0号纹理单元传递给取样器变量
    gl.uniform1i(u_Sampler, 0);

    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);

}

示例链接:
1、纹理加载基础示例
2、平铺式纹理映射
3、镜像纹理映射
4、使用多张纹理贴图

基于WebGL纹理映射与光照渲染是WebGL中常用的图形渲染技术之一。纹理映射是将一张图片贴到一个三维模型表面上,使得模型表面呈现出图片的样子。光照渲染是通过计算光线在三维模型表面上的反射和折射,来模拟真实世界中的光照效果。下面是一个基于WebGL纹理映射与光照渲染的例子: ```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>WebGL Texture Mapping and Lighting</title> <style> canvas { border: 1px solid black; } </style> </head> <body> <canvas id="myCanvas" width="400" height="400"></canvas> <script> var gl; var shaderProgram; var vertexBuffer; var indexBuffer; var texture; var textureImage; var angle = 0; function initGL(canvas) { try { gl = canvas.getContext("webgl"); gl.viewportWidth = canvas.width; gl.viewportHeight = canvas.height; } catch (e) { console.log(e); } if (!gl) { console.log("Could not initialise WebGL"); } } function initShaders() { var vertexShaderSource = ` attribute vec3 aVertexPosition; attribute vec2 aTextureCoord; attribute vec3 aVertexNormal; uniform mat4 uMVMatrix; uniform mat4 uPMatrix; uniform mat4 uNMatrix; varying vec2 vTextureCoord; varying vec3 vTransformedNormal; varying vec4 vPosition; void main(void) { vPosition = uMVMatrix * vec4(aVertexPosition, 1.0); gl_Position = uPMatrix * vPosition; vTextureCoord = aTextureCoord; vTransformedNormal = vec3(uNMatrix * vec4(aVertexNormal, 1.0)); } `; var fragmentShaderSource = ` precision mediump float; varying vec2 vTextureCoord; varying vec3 vTransformedNormal; varying vec4 vPosition; uniform sampler2D uSampler; uniform vec3 uAmbientColor; uniform vec3 uLightingDirection; uniform vec3 uDirectionalColor; void main(void) { vec3 ambientLight = uAmbientColor; vec3 directionalLightColor = vec3(0.0, 0.0, 0.0); vec3 directionalVector = normalize(uLightingDirection); float directional = max(dot(vTransformedNormal, directionalVector), 0.0); if (directional > 0.0) { directionalLightColor = uDirectionalColor * directional; } vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); gl_FragColor = vec4(textureColor.rgb * (ambientLight + directionalLightColor), textureColor.a); } `; var vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, vertexShaderSource); gl.compileShader(vertexShader); if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { console.log(gl.getShaderInfoLog(vertexShader)); } var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, fragmentShaderSource); gl.compileShader(fragmentShader); if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { console.log(gl.getShaderInfoLog(fragmentShader)); } shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { console.log(gl.getProgramInfoLog(shaderProgram)); } gl.useProgram(shaderProgram); shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); shaderProgram.textureCoordAttribute = gl.getAttribLocation(shaderProgram, "aTextureCoord"); gl.enableVertexAttribArray(shaderProgram.textureCoordAttribute); shaderProgram.vertexNormalAttribute = gl.getAttribLocation(shaderProgram, "aVertexNormal"); gl.enableVertexAttribArray(shaderProgram.vertexNormalAttribute); shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix"); shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix"); shaderProgram.nMatrixUniform = gl.getUniformLocation(shaderProgram, "uNMatrix"); shaderProgram.samplerUniform = gl.getUniformLocation(shaderProgram, "uSampler"); shaderProgram.ambientColorUniform = gl.getUniformLocation(shaderProgram, "uAmbientColor"); shaderProgram.lightingDirectionUniform = gl.getUniformLocation(shaderProgram, "uLightingDirection"); shaderProgram.directionalColorUniform = gl.getUniformLocation(shaderProgram, "uDirectionalColor"); } function initBuffers() { var vertices = [ // Front face -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, // Back face -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, // Top face -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, // Bottom face -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, // Right face 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, // Left face -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0 ]; var textureCoords = [ // Front face 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, // Back face 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, // Top face 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, // Bottom face 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // Right face 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, // Left face 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 ]; var normals = [ // Front face 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // Back face 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, // Top face 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // Bottom face 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, // Right face 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, // Left face -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0 ]; var indices = [ 0, 1, 2, 0, 2, 3, // Front face 4, 5, 6, 4, 6, 7, // Back face 8, 9, 10, 8, 10, 11, // Top face 12, 13, 14, 12, 14, 15, // Bottom face 16, 17, 18, 16, 18, 19, // Right face 20, 21, 22, 20, 22, 23 // Left face ]; vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); vertexBuffer.itemSize = 3; vertexBuffer.numItems = 24; indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); indexBuffer.numItems = 36; textureCoordsBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordsBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW); textureCoordsBuffer.itemSize = 2; textureCoordsBuffer.numItems = 24; normalBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW); normalBuffer.itemSize = 3; normalBuffer.numItems = 24; } function initTexture() { texture = gl.createTexture(); textureImage = new Image(); textureImage.onload = function() { handleLoadedTexture(texture, textureImage); } textureImage.src = "texture.png"; } function handleLoadedTexture(texture, textureImage) { gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImage); gl.texParameteri(gl.TEXTURE_2D, gl
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值