three.js实战-shader实现antv L7城市扫光效果

1. 需要掌握的知识

  1. webGL shader编程
  2. three.js基础用法

2. antv L7城市扫光效果

在这里插入图片描述

上图我们可以看到一个个的光波从城市中心像远处扩散,光波略过的高楼与地面会呈现高亮的状态,并且根据光波的宽度(大圈的半径 - 小圈的半径)高亮还呈现出渐变的状态。

WebGL编程指南-27 逐片元处理点光源光照效果我们知道,点光源照射在物体上的明暗效果,是通过该片元到顶点距离跟顶点法线来决定的,那我们可以借此思路来实现,大致思路是一个不断变大的圆,这个圆分大圆与小圆,在大圆与小圆之间的片元呈高光效果

3. 代码实现

我们借助three.js官网提供的一个例子画出城市,在此例子基础上实现扫光效果

3.1 three.js demo效果

在这里插入图片描述

1. 首先给城市添加地面

const planeGeo = new THREE.PlaneBufferGeometry(1700, 1700);

   			ground_shadermaterial = new THREE.ShaderMaterial({
   				uniforms: {
   					//小圆圈半径
   					innerCircleWidth: {
   						value: 0,
   					},
   					//大圆圈半径=innerCircleWidth + circleWidth
   					circleWidth: {
   						value: shaderCircleWidth,
   					},
   					diff: {
   						value: new THREE.Color(0.2, 0.2, 0.2),
   					},
   					color: {
   						value: new THREE.Color(0.0, 0.0, 1.0),
   					},
   					opacity: {
   						value: 0.3,
   					},
   					//圆心位置
   					center: {
   						value: new THREE.Vector3(0, 0, 0),
   					},
   				},
   				vertexShader: ground_vertexShader,
   				fragmentShader: ground_fragmentShader,
   				// side: THREE.DoubleSide,              // 双面可见
   				transparent: true,
   			});

   			const ground = new THREE.Mesh(planeGeo, ground_shadermaterial);

   			ground.rotation.x = -Math.PI / 2;
   			scene.add(ground);

2. 给大楼添加纹理贴图

	const geometry = new THREE.BoxGeometry(1, 1, 1);
				geometry.translate(0, 0.5, 0);
				//添加纹理
				// const material = new THREE.MeshPhongMaterial({
				// 	color: 0xffffff,
				// 	flatShading: true,
				// 	map: new THREE.TextureLoader().load("./textures/building.png"),
				// });
				//顶点着色器
				const vertexShader = `
			   
			    varying vec2 vUv;
                varying vec3 v_position;
                void main() {
                vUv = uv;
                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
				v_position = vec3(modelMatrix * vec4(position, 1.0));
                }
			   `;
               //片元着色器
				const fragmentShader = `
			     varying vec2 vUv;
                 varying vec3 v_position;

                 uniform float innerCircleWidth;
                 uniform float circleWidth;
                 uniform float opacity;
                 uniform vec3 center;
        
                 uniform vec3 color;
                 uniform sampler2D buliding;

                 void main() {
                 float dis = length(v_position - center);
                 if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
                    float r = (dis - innerCircleWidth) / circleWidth;
                    vec4 tex = texture2D( buliding, vUv);
                    gl_FragColor = mix(tex, vec4(color, opacity), r);
                 }else {
                   gl_FragColor = texture2D( buliding, vUv);
                }  
               }
			   `;

				const map = new THREE.TextureLoader().load("./textures/building.png");
				map.wrapS = THREE.RepeatWrapping;
				map.wrapT = THREE.RepeatWrapping;
				material = new THREE.ShaderMaterial({
					uniforms: {
						buliding: {
							value: map,
						},
						innerCircleWidth: {
							value: 0,
						},
						circleWidth: {
							value: shaderCircleWidth,
						},
						color: {
							value: new THREE.Color(0.0, 0.0, 1.0),
						},
						opacity: {
							value: 0.9,
						},
						center: {
							value: new THREE.Vector3(0, 0, 0),
						},
					},
					vertexShader: vertexShader,
					fragmentShader: fragmentShader,
					// side: THREE.DoubleSide,              // 双面可见
					transparent: true,
				});

3. 修改相机可视范围

由于demo的相机可视范围太小,为了方便查看效果更改一下相机范围

				controls.minDistance = 100;
				controls.maxDistance = 5000;

4.核心逻辑

我们在 ground_fragmentShader(地面的着色器)中发现此片元的颜色,取决于该片元到地图中心(center)的距离是否在光波范围(circleWidth属性),下面用到了mix函数这是一个插值函数,为了让我们的高亮与片元在光波中的位置有渐变效果,我们用此方法

float dis = length(v_position - center);
                  if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
                    float r = (dis - innerCircleWidth) / circleWidth;
               
                  gl_FragColor = mix(vec4(diff, 0.1), vec4(color, opacity), r);
                  }else {
                  gl_FragColor = vec4(diff, 0.1);

fragmentShader(高楼的着色器)逻辑类似取当前片元的纹素通过mix函数求出一个“高亮”颜色

  if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
                    float r = (dis - innerCircleWidth) / circleWidth;
                    vec4 tex = texture2D( buliding, vUv);
                    gl_FragColor = mix(tex, vec4(color, opacity), r);
                 }else {
                   gl_FragColor = texture2D( buliding, vUv);
                }  

最后在animate函数中改变innerCircleWidth的大小,使其不断变大(也就是小圆的半径不断变大)

function animate() {
				requestAnimationFrame(animate);

				controls.update(); // only required if controls.enableDamping = true, or if controls.autoRotate = true
				//这里的20就是圆半径的扩大速度
				material.uniforms.innerCircleWidth.value += 20;
				if (material.uniforms.innerCircleWidth.value > 1700) {
					material.uniforms.innerCircleWidth.value = -shaderCircleWidth;
				}
				ground_shadermaterial.uniforms.innerCircleWidth.value += 20;
				if (ground_shadermaterial.uniforms.innerCircleWidth.value > 1700) {
					ground_shadermaterial.uniforms.innerCircleWidth.value =
						-shaderCircleWidth;
				}
				render();
			}

5.demo效果

在这里插入图片描述

6.demo代码

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - postprocessing - unreal bloom</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<style>
			body {
				color: #fff;
				font-family:Monospace;
				font-size:13px;
				text-align:center;
				background-color: #fff;
				margin: 0px;
				overflow: hidden;
			}
        </style>
        <script src="./three.js"></script>
        <script src="./OrbitControls.js"></script>
        <script src="./BufferGeometryUtils.js"></script>
    </head>
    <script type="x-shader/x-vertex" id="vertexShader">
        varying vec2 vUv;
        varying vec3 v_position;
        void main() {
            vUv = uv;
            v_position = position;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      </script>
      <script type="x-shader/x-vertex" id="fragmentShader">
        varying vec2 vUv;
        varying vec3 v_position;

        uniform float innerCircleWidth;
        uniform float circleWidth;
        uniform float opacity;
        uniform vec3 center;
        
        uniform vec3 color;
        uniform sampler2D texture;

        void main() {
            float dis = length(v_position - center);
            if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
                float r = (dis - innerCircleWidth) / circleWidth;
                vec4 tex = texture2D( texture, vUv);
                gl_FragColor = mix(tex, vec4(color, opacity), r);
            }else {
                gl_FragColor = texture2D( texture, vUv);
            }
        }
      </script>

      <script type="x-shader/x-vertex" id="ground_vertexShader">
        varying vec2 vUv;
        varying vec3 v_position;
        void main() {
            vUv = uv;
            v_position = position;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      </script>
      <script type="x-shader/x-vertex" id="ground_fragmentShader">
        varying vec2 vUv;
        varying vec3 v_position;

        uniform float innerCircleWidth;
        uniform float circleWidth;
        uniform float opacity;
        uniform vec3 center;
        
        uniform vec3 color;
        uniform vec3 diff;

        void main() {
            float dis = length(v_position - center);
            if(dis < (innerCircleWidth + circleWidth) && dis > innerCircleWidth) {
                float r = (dis - innerCircleWidth) / circleWidth;
               
                gl_FragColor = mix(vec4(diff, 0.1), vec4(color, opacity), r);
            }else {
                gl_FragColor = vec4(diff, 0.1);
            }

        }
      </script>
	<body>
		<div id="container"></div>
        
		<script>
            console.log(1298371298739123)
			var scene, camera, controls, pointLight
			var renderer, shadermaterial, ground_shadermaterial, shaderCircleWidth = 300
			var clock = new THREE.Clock()
            var container = document.getElementById( 'container' )
            // 数据池,为防止轮播时请求数据出现延迟,以及点击时快速进入下一级地图
            var mapDataPool = {}
            var height = 30
            var MAP_SIZE = 1000, MAP_WIDTH = MAP_SIZE, MAP_HEIGHT = MAP_SIZE

			renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } )
			renderer.setPixelRatio( window.devicePixelRatio )
            renderer.setClearColor('#000')
			renderer.setSize( window.innerWidth, window.innerHeight )
			renderer.toneMapping = THREE.ReinhardToneMapping
			container.appendChild( renderer.domElement )

			scene = new THREE.Scene()

			camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.1, 10000 )
			camera.position.set( 200, 200, 200 )
			scene.add( camera )

			controls = new THREE.OrbitControls( camera, renderer.domElement )

             /**
             * 光源设置
             */
            // 方向光1
            var directionalLight = new THREE.DirectionalLight(0xffffff, 0.9)
            directionalLight.position.set(400, 200, 300)
            directionalLight.layers.enable(1)
            scene.add(directionalLight)

			scene.add( new THREE.AmbientLight( 0x404040 ) )
			pointLight = new THREE.PointLight( 0xffffff, 1 )
            camera.add( pointLight )
            

            var minLng = null, minLat = null, maxLng = null, maxLat = null

            var mainMapGroup = new THREE.Group()
            scene.add(mainMapGroup)

            let map = new THREE.TextureLoader().load('./building.png')
            map.wrapS = THREE.RepeatWrapping;
            map.wrapT = THREE.RepeatWrapping;
			var texMAT = new THREE.MeshBasicMaterial({ map })

            var mat = new THREE.MeshPhongMaterial({
                transparent: true, 
                // opacity: 1,
                // color: new THREE.Color(1, 1, 0),
                side: THREE.BackSide,
                map: new THREE.TextureLoader().load('./building_top.png')
            })

            var mat2 = new THREE.MeshBasicMaterial({
                transparent: true, 
                // opacity: 1,
                // color: new THREE.Color(1,0,0),
                map: new THREE.TextureLoader().load('./building_top.png')
            })

            shadermaterial = new THREE.ShaderMaterial( {
                uniforms: {
                    texture: {
                        value: map
                    },
                    innerCircleWidth: {
                        value: 0
                    },
                    circleWidth: {
                        value: shaderCircleWidth
                    },
                    color: {
                        value: new THREE.Color(0.0, 0.0, 1.0)
                    },
                    opacity: {
                        value: 0.9
                    },
                    center: {
                        value: new THREE.Vector3(0, 0, 0)
                    }
                },
                vertexShader: document.getElementById( 'vertexShader' ).textContent,
                fragmentShader: document.getElementById( 'fragmentShader' ).textContent,
                // side: THREE.DoubleSide,              // 双面可见
                transparent: true
            } );

            ground_shadermaterial = new THREE.ShaderMaterial( {
                uniforms: {
                    innerCircleWidth: {
                        value: 0
                    },
                    circleWidth: {
                        value: shaderCircleWidth
                    },
                    diff: {
                        value: new THREE.Color(0.2, 0.2, 0.2)
                    },
                    color: {
                        value: new THREE.Color(0.0, 0.0, 1.0)
                    },
                    opacity: {
                        value: 0.3
                    },
                    center: {
                        value: new THREE.Vector3(0, 0, 0)
                    }
                },
                vertexShader: document.getElementById( 'ground_vertexShader' ).textContent,
                fragmentShader: document.getElementById( 'ground_fragmentShader' ).textContent,
                // side: THREE.DoubleSide,              // 双面可见
                transparent: true
            } );
            var groundGeo = new THREE.PlaneBufferGeometry( 1000, 1000 );
            // var groundMat = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide });
            var ground = new THREE.Mesh( groundGeo, ground_shadermaterial );
            ground.rotation.x = -Math.PI/2
            scene.add( ground );

            fetch('build1.geojson')
            .then((response) => {
                return response.json();
            })
            .then((mapJson) => {
                var coor = getGeoExtent(mapJson.features)
                minLng = coor.minLng, 
                minLat = coor.minLat, 
                maxLng = coor.maxLng, 
                maxLat = coor.maxLat
                
                let geos = []
                let sideGeos = [];
                let topGeos = [];
                let topGeos2 = [];
                
                for(let i = 0;i< mapJson.features.length;i++) {
                    let feature = mapJson.features[i]
                    if (!feature.geometry) return;
                    const coordinates = feature.geometry.coordinates;
                    switch (feature.geometry.type) {
                        case 'Polygon':
                            for (let points of coordinates) {
                                const _points = points.map((point) => {
                                    return lnglat2Map(point)
                                })
                                let floor = feature.properties.Floor
                                let geometry = createBuildingGeometry(_points, floor)
                                sideGeos.push(geometry)

                                let topGeo = createTopGeo(_points, floor)
                                topGeos.push(topGeo[0])
                                topGeos2.push(topGeo[1])

                            }
                            break;    
                        case 'MultiPolygon': 
                            for(let points of coordinates[0]) {
                                const _points = points.map((point) => {
                                    return lnglat2Map(point)
                                })
                                let floor = feature.properties.Floor
                               
                                let geometry = createBuildingGeometry(_points, floor)
                                sideGeos.push(geometry)
                                
                                let topGeo = createTopGeo(_points, floor)
                                topGeos.push(topGeo[0])
                                topGeos2.push(topGeo[1])
                            }
                            break;
                        default:
                        break;
                    }
                }

                let sideGeo = THREE.BufferGeometryUtils.mergeBufferGeometries(sideGeos, false)
                // let city1 = new THREE.Mesh(sideGeo, texMAT)
                let city1 = new THREE.Mesh(sideGeo, shadermaterial)
                city1.rotation.x = -Math.PI/2
                scene.add(city1)

                let topGeo = THREE.BufferGeometryUtils.mergeBufferGeometries(topGeos, false)
                topGeo = new THREE.Geometry().fromBufferGeometry(topGeo);
                topGeo.computeFaceNormals()
                let city2= new THREE.Mesh(topGeo, mat)
                city2.rotation.x = -Math.PI/2
                scene.add(city2)

                let topGeo2 = THREE.BufferGeometryUtils.mergeBufferGeometries(topGeos2, false)
                let city3= new THREE.Mesh(topGeo2, mat2)
                city3.rotation.x = -Math.PI/2
                scene.add(city3)
                
            })

            animate()

			window.onresize = function () {
				var width = window.innerWidth;
				var height = window.innerHeight;
				camera.aspect = width / height;
				camera.updateProjectionMatrix();
				renderer.setSize( width, height );
			};
			function animate() {
				requestAnimationFrame( animate )
                renderer.render(scene, camera)

                shadermaterial.uniforms.innerCircleWidth.value += 10
                if(shadermaterial.uniforms.innerCircleWidth.value > 1000){
                    shadermaterial.uniforms.innerCircleWidth.value = -shaderCircleWidth
                }
                ground_shadermaterial.uniforms.innerCircleWidth.value += 10
                if(ground_shadermaterial.uniforms.innerCircleWidth.value > 1000){
                    ground_shadermaterial.uniforms.innerCircleWidth.value = -shaderCircleWidth
                }
            }

            function biseP(p, a, b, d) {
                let dStartAngle = Math.atan2(a.y - p.y, a.x - p.x),
                    dEndAngle = Math.atan2(b.y - p.y, b.x - p.x);
                let dWAngle = dEndAngle - dStartAngle; // 外角角度

                if (dWAngle < 0) {
                    dWAngle += 2 * Math.PI;
                } else if (dWAngle > 2 * Math.PI) {
                    dWAngle -= 2 * Math.PI;
                } // 这里算出来角度都是弧度单位的


                let θ = dWAngle / 2 + dStartAngle; // 计算角平分线上偏移距离
                // let l = d;
                // 计算垂直偏移距离

                let l = d / Math.sin(dWAngle / 2);
                if(Math.abs(l) > 2*d) l = d; // 限制异常数据
                let panX = l * Math.cos(θ) + p.x,
                    panY = l * Math.sin(θ) + p.y; // 夹角(内角)平分线的点

                return {
                    x: 2 * p.x - panX,
                    y: 2 * p.y - panY
                };
            };

            function getOffsetPoint(points, index, offset) {
                let len = points.length;
                let prePoint, nextPoint;

                if (index == 0) {
                    // 第一个点
                    prePoint = points[len - 2];
                    nextPoint = points[index + 1];
                } else if (index == len - 1) {
                    // 最后一个点
                    prePoint = points[index - 1];
                    nextPoint = points[1];
                } else {
                    prePoint = points[index - 1];
                    nextPoint = points[index + 1];
                }

                return biseP(points[index], prePoint, nextPoint, offset);
            };

            /**
             * @name 生成顶部几何体
             * @param {array} points 点集
             * @param {number} offset_s 内凹边距
             * @param {number} height 总体高度
             * @param {number} offset_h 沉降深度
             * @return {array} geometries 构造沉降+底部shape
             */
            function createTopGeo(points, floor) {
                let vertices = []; // 顶点数组
                let indices = []; // 索引数组
                if(THREE.ShapeUtils.isClockWise( points )) {
                    points = points.reverse();
                }
                let offset_h = 0.2, offset_s = 0.1, height = floor
                let len = points.length;
                let point_offests = points.map((point, index) => {
                    const point_offest = getOffsetPoint(points, index, offset_s);
                    const p1 = new THREE.Vector3(point.x, point.y, height);
                    const p2 = new THREE.Vector3(point_offest.x, point_offest.y, height);
                    const p3 = new THREE.Vector3(p2.x, p2.y, p2.z - offset_h);
                    vertices.push(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z);

                    if (index != len - 1) {
                        indices.push(index * 3, index * 3 + 1, (index + 1) * 3 + 1, // 0, 1, 4
                        (index + 1) * 3 + 1, (index + 1) * 3, index * 3, // 4, 3, 0
                        index * 3 + 1, index * 3 + 2, (index + 1) * 3 + 2, // 1, 2, 5
                        (index + 1) * 3 + 2, (index + 1) * 3 + 1, index * 3 + 1 // 5, 4, 1
                        );
                    } else {
                        // 最后一组
                        indices.push(index * 3, index * 3 + 1, 1, 1, 0, index * 3, index * 3 + 1, index * 3 + 2, 2, 2, 1, index * 3 + 1);
                    }

                    return point_offest;
                });

                let geometry = new THREE.BufferGeometry();
                geometry.setIndex(indices);
                geometry.addAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
                const geo = new THREE.ShapeBufferGeometry(new THREE.Shape(point_offests));
                const translateMatrix = new THREE.Matrix4();
                translateMatrix.makeTranslation(0, 0, height - offset_h);
                geo.applyMatrix(translateMatrix);
                return [geometry, geo];
            }

            function createBuildingGeometry(points, floor) {
                let mapInnerSize = floor ; // 贴图对应真实尺寸大小(3m)
                let vecHeight = floor;
                
                let vertices = []; // 墙壁顶点数组
                let indices = []; // 墙壁索引数组  indices index负数
                let uv = []; // 墙壁纹理坐标

                // const uv_y = vecHeight / mapInnerSize;
                const uv_y = floor
                let uv_x = 0;
                if(THREE.ShapeUtils.isClockWise( points )) {
                    points = points.reverse();
                }
                const len = points.length;
                points.forEach((point, index) => {
                    vertices.push(point.x, point.y, 0); // 底部点
                    vertices.push(point.x, point.y, vecHeight); // 顶部点

                    if (index !== 0) {
                        const prePoint = points[index - 1];
                        const dis = new THREE.Vector2(prePoint.x, prePoint.y).distanceTo(new THREE.Vector2(point.x, point.y));

                        uv_x += dis*2 / mapInnerSize;
                        // uv_x += 1
                    }

                    uv.push(uv_x, 0, uv_x, uv_y);

                    if (index !== 0) {
                        // 推入面索引
                        indices.push(index * 2 - 2, index * 2, index * 2 - 1, index * 2 - 1, index * 2, index * 2 + 1);
                    }
                });

                // 闭合
                const firstPoint = points[0], lastPoint = points[len - 1];
                vertices.push(firstPoint.x, firstPoint.y, 0); // 底部点
                vertices.push(firstPoint.x, firstPoint.y, vecHeight); // 顶部点

                const dis = new THREE.Vector2(firstPoint.x, firstPoint.y).distanceTo(new THREE.Vector2(lastPoint.x, lastPoint.y));

                uv_x += dis / mapInnerSize;
                uv.push(uv_x, 0, uv_x, uv_y);

                indices.push(len * 2 - 2, len * 2, len * 2 - 1, len * 2 - 1, len * 2, len * 2 + 1);


                // 构建侧面
                let geometry = new THREE.BufferGeometry();
                geometry.isBufferGeometry = true
                geometry.setIndex(indices);
                geometry.addAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
                geometry.addAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));

                return geometry;
            }
           
            function getGeoExtent(features) { // 计算数据的最大最小经纬度、最大最小墨卡托坐标以及墨卡托坐标的的多变形数组
                let minLng = 180, maxLng = -180, minLat = 90, maxLat = -90
                for (let feature of features) {
                    if (feature.geometry) {
                        if (feature.geometry.type === 'Polygon') {
                            for (let points of feature.geometry.coordinates) {
                                for (let point of points) {
                                    minLng = minLng < point[0] ? minLng : point[0];
                                    maxLng = maxLng > point[0] ? maxLng : point[0];
                                    minLat = minLat < point[1] ? minLat : point[1];
                                    maxLat = maxLat > point[1] ? maxLat : point[1];
                                }
                            }
                        } else if (feature.geometry.type === 'MultiPolygon') {
                            for (let polygonPoints of feature.geometry.coordinates) {
                                for (let points of polygonPoints) {
                                    for (let point of points) {
                                        minLng = minLng < point[0] ? minLng : point[0];
                                        maxLng = maxLng > point[0] ? maxLng : point[0];
                                        minLat = minLat < point[1] ? minLat : point[1];
                                        maxLat = maxLat > point[1] ? maxLat : point[1];
                                    }
                                }
                            }
                        }
                    }
                }
                return { minLng, minLat, maxLng, maxLat }
            }

            function lnglat2Map(lnglat) {
                
                let v = new THREE.Vector2(
                    ((lnglat[0] - minLng) / (maxLng - minLng)) * MAP_WIDTH - MAP_WIDTH * 0.5,
                    ((lnglat[1] - minLat) / (maxLat - minLat)) * MAP_HEIGHT - MAP_HEIGHT * 0.5
                    )
                return v
            }

		</script>

	</body>

</html>
  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值