使用ThreeJs从零开始构建3D智能仓库——第三章(选中物体与特效)

写在前面——目录结构

这一章我们来完成激动人心的,关于如何鼠标单击选中一个物体,并让该物体周围闪烁白光,并在鼠标点击位置的上方显示该物体的名字。如下图所示:
2019.11.26 更新:我最近建立了个人网站,大家可以访问下面的链接查看演示
3D仓库演示
2019.11.28 更新:代码和图片资源等已上传至GitHub
https://github.com/xiao149/ThreeJsDemo
在这里插入图片描述
在开始这章内容的讲解之前,我想先给大家推荐下项目的目录结构,因为这一章我们将要引入自己写的一个JS文件ThreeJs_Composer.js,该JS的作用就是添加选中特效和显示物体的名字等等一系列功能。我们这个3D仓库未来将会用到很多不同的功能,全部写进主页面的HTML会导致代码过长,可阅读性下降。所以我推荐大家将不同的功能封装成不同的JS(比如选中的效果、拖动物体、显示提示信息、加载模型等等),需要用到的时候再导入这些JS就会方便易懂很多。
我的目录结构如下:
在这里插入图片描述
稍微介绍下,layui是一个开源的UI框架,最近我正在研究,前台向来是我这种初学者的痛啊!现在还用不到,大家略过。ThreeJs放着我们需要用的一切,images存放图片,js存放ThreeJs原生的JS,pace是第三方的加载控件,这里也不用管,剩下的是我们自己创建的JS和主页面test.html,今天我们主要来研究ThreeJs_Composer.js这个文件。

如何选中物体

选中物体的原理

鉴于我也是从各种百度到的地方了解到这些内容的,只能说是知其然而不知其所以然,再次我就仅仅描述我所知道的东西,若有所纰漏还请原谅~~
选中一个物体我们需要依靠Raycaster这个工具,其实质是在你鼠标所指的地方(如下图)发射一条射线,对我们可以想象一条直线,垂直你的电脑屏幕,在你鼠标的位置,从屏幕外直直指向屏幕内,系统将会获取到该射线依次经过的物体。
在这里插入图片描述
举个栗子,我们可以在浏览器中按下F12打开调试模式,我这时点击了一个窗户,进入断点在我红色箭头所指地方的intersects便保存了射线依次经过的物体,分别是窗户、地面、地面。
在这里插入图片描述
在这里插入图片描述
换一个比较刁钻的角度,这次我点击了最左侧的门,由下图可见射线返回了两个物体,分别是左门1和左边的那个墙面:
在这里插入图片描述

选中物体的实现

经过上面的描述,我想大家对如何点击选中一个物体有了初步的了解,现在我们可以来创建ThreeJs_Composer这个自定义的JS了:

/*
  * 需要在jsp中导入的包
  <script src="./ThreeJs/three.js"></script>
  <script src="./ThreeJs/EffectComposer.js"></script>
  <script src="./ThreeJs/RenderPass.js"></script>
  <script src="./ThreeJs/OutlinePass.js"></script>
  <script src="./ThreeJs/FXAAShader.js"></script>
  <script src="./ThreeJs/ShaderPass.js"></script>
  <script src="./ThreeJs/CopyShader.js"></script>
  */

THREE.ThreeJs_Composer = function ( _renderer, _scene, _camera) {
    var raycaster = new THREE.Raycaster();
  	var mouse = new THREE.Vector2();
    var selectedObjects = [];

    window.addEventListener( 'click', onMouseClick);
    
    function onMouseClick( event ) {
        var x, y;
        if ( event.changedTouches ) {
            x = event.changedTouches[ 0 ].pageX;
            y = event.changedTouches[ 0 ].pageY;
        } else {
            x = event.clientX;
            y = event.clientY;
        }
        mouse.x = ( x / window.innerWidth ) * 2 - 1;
        mouse.y = - ( y / window.innerHeight ) * 2 + 1;
        raycaster.setFromCamera( mouse, _camera );
        var intersects = raycaster.intersectObjects( [ _scene ], true );

        if(intersects.length == 0){
            $("#label").attr("style","display:none;");//隐藏说明性标签
            return;
        }
        if(intersects[0].object.name == "地面" || (intersects[0].object.name == "") || (intersects[0].object.name == "墙面")){
            $("#label").attr("style","display:none;");//隐藏说明性标签
            selectedObjects.pop();
        }else{
            $("#label").attr("style","display:block;");// 显示说明性标签
            $("#label").css({left: x, top: y-40});// 修改标签的位置
            $("#label").text(intersects[0].object.name);// 显示模型信息

            selectedObjects.pop();
            selectedObjects.push( intersects[0].object );
        }
    }
}

我来简单介绍下,首先需要传入的三个参数_renderer, _scene, _camera分别是HTML中创建好的渲染器,场景和相机,我们需要在test.html中导入这个JS就像这样

<script src="./ThreeJs/ThreeJs_Composer.js"></script>

然后在init()方法中加入(这里的test.html依旧是第二章所讲的那个,忘记的同学可以回到第二章回顾下)

//添加选中时的蒙版
new THREE.ThreeJs_Composer(renderer, scene, camera);

回到我们创建的JS中,下面这个变量已经保存了我们鼠标点击处发出射线所依次经过的物体:

var intersects = raycaster.intersectObjects( [ _scene ], true );

接下来我们会有三种情况:
1.鼠标点击的地方啥都没有,就直接隐藏我们的说明性标签(就是显示在鼠标上面的那个框),直接return。

if(intersects.length == 0){
    $("#label").attr("style","display:none;");//隐藏说明性标签
    return;
}

2.点击到了地面或者墙面,这种情况下我们一般不显示说明,并且把selectedObjects这个数组清空,这个数组将会存放我们选中的物体,方便之后添加发光特效。

if(intersects[0].object.name == "地面" || (intersects[0].object.name == "") || (intersects[0].object.name == "墙面")){
    $("#label").attr("style","display:none;");//隐藏说明性标签
    selectedObjects.pop();
}

3.点击到了门窗或者其他能够选中的物体,这种情况下显示说明(物体的名字),并且把selectedObjects这个数组清空后赋值。

else{
    $("#label").attr("style","display:block;");// 显示说明性标签
    $("#label").css({left: x, top: y-40});// 修改标签的位置
    $("#label").text(intersects[0].object.name);// 显示模型信息

    selectedObjects.pop();
    selectedObjects.push( intersects[0].object );
}

完成后效果图如下(鼠标点击了左门1):
在这里插入图片描述

添加选中后的发光特效

这个需要我们用到ThreeJs的后期处理THREE.EffectComposer,以及一系列“通道”,THREE.RenderPassTHREE.OutlinePassTHREE.ShaderPass
我们在之前的自定义JS中修改如下:

/*
  * 需要在jsp中导入的包
  <script src="./ThreeJs/three.js"></script>
  <script src="./ThreeJs/EffectComposer.js"></script>
  <script src="./ThreeJs/RenderPass.js"></script>
  <script src="./ThreeJs/OutlinePass.js"></script>
  <script src="./ThreeJs/FXAAShader.js"></script>
  <script src="./ThreeJs/ShaderPass.js"></script>
  <script src="./ThreeJs/CopyShader.js"></script>
  */

THREE.ThreeJs_Composer = function ( _renderer, _scene, _camera) {
    var raycaster = new THREE.Raycaster();
  	var mouse = new THREE.Vector2();
    var composer = new THREE.EffectComposer( _renderer );
    var renderPass = new THREE.RenderPass( _scene, _camera );
    var selectedObjects = [];
    composer.addPass( renderPass );
    var outlinePass = new THREE.OutlinePass( new THREE.Vector2( window.innerWidth, window.innerHeight ), _scene, _camera );
    outlinePass.edgeStrength = 5;//包围线浓度
    outlinePass.edgeGlow = 0.5;//边缘线范围
    outlinePass.edgeThickness = 2;//边缘线浓度
    outlinePass.pulsePeriod = 2;//包围线闪烁频率
    outlinePass.visibleEdgeColor.set( '#ffffff' );//包围线颜色
    outlinePass.hiddenEdgeColor.set( '#190a05' );//被遮挡的边界线颜色
    composer.addPass( outlinePass );
    var effectFXAA = new THREE.ShaderPass( THREE.FXAAShader );
    effectFXAA.uniforms[ 'resolution' ].value.set( 1 / window.innerWidth, 1 / window.innerHeight );
    effectFXAA.renderToScreen = true;
    composer.addPass( effectFXAA );

    window.addEventListener( 'click', onMouseClick);

    function onMouseClick( event ) {
        var x, y;
        if ( event.changedTouches ) {
            x = event.changedTouches[ 0 ].pageX;
            y = event.changedTouches[ 0 ].pageY;
        } else {
            x = event.clientX;
            y = event.clientY;
        }
        mouse.x = ( x / window.innerWidth ) * 2 - 1;
        mouse.y = - ( y / window.innerHeight ) * 2 + 1;
        raycaster.setFromCamera( mouse, _camera );
        var intersects = raycaster.intersectObjects( [ _scene ], true );

        if(intersects.length == 0){
            $("#label").attr("style","display:none;");//隐藏说明性标签
            return;
        }
        if(intersects[0].object.name == "地面" || (intersects[0].object.name == "") || (intersects[0].object.name == "墙面")){
            $("#label").attr("style","display:none;");//隐藏说明性标签
            selectedObjects.pop();
        }else{
            $("#label").attr("style","display:block;");// 显示说明性标签
            $("#label").css({left: x, top: y-40});// 修改标签的位置
            $("#label").text(intersects[0].object.name);// 显示模型信息

            selectedObjects.pop();
            selectedObjects.push( intersects[0].object );
            outlinePass.selectedObjects = selectedObjects;//给选中的线条和物体加发光特效
        }
    }

    return composer;
}

关于后期通道这方面我了解的很少,有兴趣的同学可以自行百度,官网上也有很多栗子,比如雪花特效啊,变暗变亮的特效啊等等。这里修改后的JS和之前章节创建的差不多,白色光圈的特效主要依靠OutlinePass通道,我们将射线获取到的selectedObjects赋给outlinePass就可以实现文章开头演示的效果啦!其中下面的这些参数大家可以依据自己的喜好适度修改。

outlinePass.edgeStrength = 5;//包围线浓度
outlinePass.edgeGlow = 0.5;//边缘线范围
outlinePass.edgeThickness = 2;//边缘线浓度
outlinePass.pulsePeriod = 2;//包围线闪烁频率
outlinePass.visibleEdgeColor.set( '#ffffff' );//包围线颜色
outlinePass.hiddenEdgeColor.set( '#190a05' );//被遮挡的边界线颜色

HTML更新如下

<!DOCTYPE html>
<html>

<head includeDefault="true">
  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  <title>3D库图显示</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
    }

    #label {
      position: absolute;
      padding: 10px;
      background: rgba(255, 255, 255, 0.6);
      line-height: 1;
      border-radius: 5px;
    }
  </style>
  <script src="./ThreeJs/js/three.js"></script>
  <script src="./ThreeJs/js/stats.min.js"></script>
  <script src="./ThreeJs/js/DragControls.js"></script>
  <script src="./ThreeJs/js/OrbitControls.js"></script>
  <script src="./ThreeJs/js/dat.gui.min.js"></script>
  <script src="./ThreeJs/js/EffectComposer.js"></script>
  <script src="./ThreeJs/js/RenderPass.js"></script>
  <script src="./ThreeJs/js/OutlinePass.js"></script>
  <script src="./ThreeJs/js/FXAAShader.js"></script>
  <script src="./ThreeJs/js/CopyShader.js"></script>
  <script src="./ThreeJs/js/ShaderPass.js"></script>
  <script src="./ThreeJs/js/OBJLoader.js"></script>
  <script src="./ThreeJs/js/MTLLoader.js"></script>
  <script src="./ThreeJs/js/ThreeBSP.js"></script>
  <script src="./ThreeJs/ThreeJs_Composer.js"></script>
  <script src="./ThreeJs/js/jquery-1.11.0.min.js"></script>
</head>

<body>
  <div id="label"></div>
  <div id="container"></div>
  <script>
    var stats = initStats();
    var scene, camera, renderer, controls, light, composer;
    var matArrayA = []; //内墙
    var matArrayB = []; //外墙
    var group = new THREE.Group();

    // 初始化场景
    function initScene() {
      scene = new THREE.Scene();
      scene.fog = new THREE.Fog(scene.background, 3000, 5000);
    }

    // 初始化相机
    function initCamera() {
      camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
      camera.position.set(0, 800, 1500);
      camera.lookAt(new THREE.Vector3(0, 0, 0));
    }

    // 初始化灯光
    function initLight() {
      var directionalLight = new THREE.DirectionalLight(0xffffff, 0.3); //模拟远处类似太阳的光源
      directionalLight.color.setHSL(0.1, 1, 0.95);
      directionalLight.position.set(0, 200, 0).normalize();
      scene.add(directionalLight);

      var ambient = new THREE.AmbientLight(0xffffff, 1); //AmbientLight,影响整个场景的光源
      ambient.position.set(0, 0, 0);
      scene.add(ambient);
    }

    // 初始化性能插件
    function initStats() {
      var stats = new Stats();

      stats.domElement.style.position = 'absolute';
      stats.domElement.style.left = '0px';
      stats.domElement.style.top = '0px';

      document.body.appendChild(stats.domElement);
      return stats;
    }

    // 初始化渲染器
    function initRenderer() {
      renderer = new THREE.WebGLRenderer({
        antialias: true
      });
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setClearColor(0x4682B4, 1.0);
      document.body.appendChild(renderer.domElement);
    }

    //创建地板
    function createFloor() {
      var loader = new THREE.TextureLoader();
      loader.load("./ThreeJs/images/floor.jpg", function(texture) {
        texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
        texture.repeat.set(10, 10);
        var floorGeometry = new THREE.BoxGeometry(2600, 1400, 1);
        var floorMaterial = new THREE.MeshBasicMaterial({
          map: texture,
          side: THREE.DoubleSide
        });
        var floor = new THREE.Mesh(floorGeometry, floorMaterial);
        floor.position.y = -0.5;
        floor.rotation.x = Math.PI / 2;
        floor.name = "地面";
        scene.add(floor);
      });
    }

    //创建墙
    function createCubeWall(width, height, depth, angle, material, x, y, z, name) {
      var cubeGeometry = new THREE.BoxGeometry(width, height, depth);
      var cube = new THREE.Mesh(cubeGeometry, material);
      cube.position.x = x;
      cube.position.y = y;
      cube.position.z = z;
      cube.rotation.y += angle * Math.PI; //-逆时针旋转,+顺时针
      cube.name = name;
      scene.add(cube);
    }

    //创建门_左侧
    function createDoor_left(width, height, depth, angle, x, y, z, name) {
      var loader = new THREE.TextureLoader();
      loader.load("./ThreeJs/images/door_left.png", function(texture) {
        var doorgeometry = new THREE.BoxGeometry(width, height, depth);
        doorgeometry.translate(50, 0, 0);
        var doormaterial = new THREE.MeshBasicMaterial({
          map: texture,
          color: 0xffffff
        });
        doormaterial.opacity = 1.0;
        doormaterial.transparent = true;
        var door = new THREE.Mesh(doorgeometry, doormaterial);
        door.position.set(x, y, z);
        door.rotation.y += angle * Math.PI; //-逆时针旋转,+顺时针
        door.name = name;
        scene.add(door);
      });
    }

    //创建门_右侧
    function createDoor_right(width, height, depth, angle, x, y, z, name) {
      var loader = new THREE.TextureLoader();
      loader.load("./ThreeJs/images/door_right.png", function(texture) {
        var doorgeometry = new THREE.BoxGeometry(width, height, depth);
        doorgeometry.translate(-50, 0, 0);
        var doormaterial = new THREE.MeshBasicMaterial({
          map: texture,
          color: 0xffffff
        });
        doormaterial.opacity = 1.0;
        doormaterial.transparent = true;
        var door = new THREE.Mesh(doorgeometry, doormaterial);
        door.position.set(x, y, z);
        door.rotation.y += angle * Math.PI; //-逆时针旋转,+顺时针
        door.name = name;
        scene.add(door);
      });
    }

    //创建窗户
    function createWindow(width, height, depth, angle, x, y, z, name) {
      var loader = new THREE.TextureLoader();
      loader.load("./ThreeJs/images/window.png", function(texture) {
        var windowgeometry = new THREE.BoxGeometry(width, height, depth);
        var windowmaterial = new THREE.MeshBasicMaterial({
          map: texture,
          color: 0xffffff
        });
        windowmaterial.opacity = 1.0;
        windowmaterial.transparent = true;
        var window = new THREE.Mesh(windowgeometry, windowmaterial);
        window.position.set(x, y, z);
        window.rotation.y += angle * Math.PI; //-逆时针旋转,+顺时针
        window.name = name;
        scene.add(window);
      });
    }

    //返回墙对象
    function returnWallObject(width, height, depth, angle, material, x, y, z, name) {
      var cubeGeometry = new THREE.BoxGeometry(width, height, depth);
      var cube = new THREE.Mesh(cubeGeometry, material);
      cube.position.x = x;
      cube.position.y = y;
      cube.position.z = z;
      cube.rotation.y += angle * Math.PI;
      cube.name = name;
      return cube;
    }

    //墙上挖门,通过两个几何体生成BSP对象
    function createResultBsp(bsp, objects_cube) {
      var material = new THREE.MeshPhongMaterial({
        color: 0x9cb2d1,
        specular: 0x9cb2d1,
        shininess: 30,
        transparent: true,
        opacity: 1
      });
      var BSP = new ThreeBSP(bsp);
      for (var i = 0; i < objects_cube.length; i++) {
        var less_bsp = new ThreeBSP(objects_cube[i]);
        BSP = BSP.subtract(less_bsp);
      }
      var result = BSP.toMesh(material);
      result.material.flatshading = THREE.FlatShading;
      result.geometry.computeFaceNormals(); //重新计算几何体侧面法向量
      result.geometry.computeVertexNormals();
      result.material.needsUpdate = true; //更新纹理
      result.geometry.buffersNeedUpdate = true;
      result.geometry.uvsNeedUpdate = true;
      scene.add(result);
    }

    //创建墙纹理
    function createWallMaterail(){
        matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //前  0xafc0ca :灰色
        matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //后
        matArrayA.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));  //上  0xd6e4ec: 偏白色
        matArrayA.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));  //下
        matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //左    0xafc0ca :灰色
        matArrayA.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //右

        matArrayB.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //前  0xafc0ca :灰色
        matArrayB.push(new THREE.MeshPhongMaterial({color: 0x9cb2d1}));  //后  0x9cb2d1:淡紫
        matArrayB.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));  //上  0xd6e4ec: 偏白色
        matArrayB.push(new THREE.MeshPhongMaterial({color: 0xd6e4ec}));  //下
        matArrayB.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //左   0xafc0ca :灰色
        matArrayB.push(new THREE.MeshPhongMaterial({color: 0xafc0ca}));  //右
    }
    
    // 初始化模型
    function initContent() {
      createFloor();
      createWallMaterail();
      createCubeWall(10, 200, 1400, 0, matArrayB, -1295, 100, 0, "墙面");
      createCubeWall(10, 200, 1400, 1, matArrayB, 1295, 100, 0, "墙面");
      createCubeWall(10, 200, 2600, 1.5, matArrayB, 0, 100, -700, "墙面");
      //创建挖了门的墙
      var wall = returnWallObject(2600, 200, 10, 0, matArrayB, 0, 100, 700, "墙面");
      var door_cube1 = returnWallObject(200, 180, 10, 0, matArrayB, -600, 90, 700, "前门1");
      var door_cube2 = returnWallObject(200, 180, 10, 0, matArrayB, 600, 90, 700, "前门2");
      var window_cube1 = returnWallObject(100, 100, 10, 0, matArrayB, -900, 90, 700, "窗户1");
      var window_cube2 = returnWallObject(100, 100, 10, 0, matArrayB, 900, 90, 700, "窗户2");
      var window_cube3 = returnWallObject(100, 100, 10, 0, matArrayB, -200, 90, 700, "窗户3");
      var window_cube4 = returnWallObject(100, 100, 10, 0, matArrayB, 200, 90, 700, "窗户4");
      var objects_cube = [];
      objects_cube.push(door_cube1);
      objects_cube.push(door_cube2);
      objects_cube.push(window_cube1);
      objects_cube.push(window_cube2);
      objects_cube.push(window_cube3);
      objects_cube.push(window_cube4);
      createResultBsp(wall, objects_cube);
      //为墙面安装门
      createDoor_left(100, 180, 2, 0, -700, 90, 700, "左门1");
      createDoor_right(100, 180, 2, 0, -500, 90, 700, "右门1");
      createDoor_left(100, 180, 2, 0, 500, 90, 700, "左门2");
      createDoor_right(100, 180, 2, 0, 700, 90, 700, "右门2");
      //为墙面安装窗户
      createWindow(100, 100, 2, 0, -900, 90, 700, "窗户");
      createWindow(100, 100, 2, 0, 900, 90, 700, "窗户");
      createWindow(100, 100, 2, 0, -200, 90, 700, "窗户");
      createWindow(100, 100, 2, 0, 200, 90, 700, "窗户");
    }

    // 初始化轨迹球控件
    function initControls() {
      controls = new THREE.OrbitControls(camera, renderer.domElement);
      controls.enableDamping = true;
      controls.dampingFactor = 0.5;
      // 视角最小距离
      controls.minDistance = 100;
      // 视角最远距离
      controls.maxDistance = 5000;
      // 最大角度
      controls.maxPolarAngle = Math.PI / 2.2;
    }

    // 更新控件
    function update() {
      stats.update();
      controls.update();
    }

    // 初始化
    function init() {
      initScene();
      initCamera();
      initRenderer();
      initContent();
      initLight();
      initControls();

      //添加选中时的蒙版
      composer = new THREE.ThreeJs_Composer(renderer, scene, camera);
      document.addEventListener('resize', onWindowResize, false);
    }

    // 窗口变动触发的方法
    function onWindowResize() {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    }

    function animate() {
      requestAnimationFrame(animate);
      renderer.render(scene, camera);
      composer.render();
      update();
    }

    init();
    animate();
  </script>
</body>

</html>

结束语

码着码着就到十一点半了,最近项目快上线了比较忙,工作时间基本都在处理项目上的琐事,也只有牺牲下晚上玩游戏的时间来更新文章了(滑稽),本章我们讲解了如何选中一个物体并添加特效。下一章我们将会推出如何给门添加打开关闭的动画,以及如何添加库位,敬请期待!
我跟广大学习ThreeJs的初学者一样,仍带着懵懂的心去探索这片新大陆,CSDN上的许多前辈都给了我很多关键的灵感和技术方法,如果大家有兴趣,也可以互相交流成长,欢迎大家指导咨询。PS:大家有兴趣可以点进去我的头像,陆陆续续也写了十来篇了。
链接:使用ThreeJs从零开始构建3D智能仓库——第一章: 点我跳转.
链接:使用ThreeJs从零开始构建3D智能仓库——第二章: 点我跳转.
链接:使用ThreeJs从零开始构建3D智能仓库——第三章: 点我跳转.
链接:使用ThreeJs从零开始构建3D智能仓库——第四章: 点我跳转.
链接:使用ThreeJs从零开始构建3D智能仓库——第五章: 点我跳转.

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值