cesium原理篇(二)--网格划分

本文深入探讨了Cesium中网格划分的实现细节,包括四叉树构建、LOD原理及Tile可见性判断等关键技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转自:http://www.cnblogs.com/fuckgiser/p/5772077.html

上一篇我们从宏观上介绍了Cesium的渲染过程,本章延续上一章的内容,详细介绍一下Cesium网格划分的一些细节,包括如下几个方面:

  • 流程
  • Tile四叉树的构建
  • LOD

流程

首先,通过上篇的类关系描述,我们可以看到,整个调度主要是update和endFrame两个函数中,前者分工,后者干活。

另外,QuadtreePrimitive类只要来维护整个地球的四叉树,而每一个Tile对应一个QuadtreeTile,另外多说一句QuadtreeTile只负责网格的维护,每一个网格对应的数据(地形&影像)则有GlobeSurfaceTile管理。

初始化的时候,当各类Provider准备就绪后,Cesium就开始构建地球,这个过程都发生在QuadtreePrimitive.prototype.update函数中。在update函数中,通过selectTilesForRendering实现网格的划分和调度。我们先根据时间和状态的变化来描述一下整个流程。

image

首先判断是否存在第零层的Tile,也就是整个四叉树的根节点。createLevelZeroTiles函数负责根节点的创建,参数tilingScheme表示地球网格采用的剖分方式,该参数值和地形的投影是一致的,因此只能是WGS1984的经纬度,也就是我们上一篇提到的剖分方式,第零层有两个根节点:

image

也就是说无论影像服务采用的是墨卡托的还是经纬度,但Cesium的地球是按照经纬度的剖分方式,和地形数据的规范保持一致。不管怎样,这样我们有了两个QuadtreeTile,一个是L0X0Y0,一个是L0X1Y0。

但此时的Tile只是一个有行列号的空壳,里面并没有实际的数据(needsLoading == true),也不能用于渲染(renderable == false)。作为一个新Tile,他被扔进了两个队列中,一个是_tileReplacementQueue,这个是一个双向链表,来统计所有加载进来的Tile,不过并没有发现它的价值,另一个是_tileLoadQueue,下载队列,该队列中的Tile,则会在下载函数中完成数据的加载,数据下载的过程会在下一篇详细介绍,此处一笔带过。

根据剧情需要,开始下载地形数据和影像数据,这个过程是异步的,当地形数据下载完成后,执行propagateNewLoadedDataToChildren函数。因为要给子节点的地形数据采样(地形中详细介绍),所以会创建出该Tile对应的四个子节点,不然还怎么叫四叉树,全部数据下载完成后就会更新该Tile的状态:

QuadtreeTile.renderable == true;

QuadtreeTile.state == QuadtreeTileLoadState.DONE;

image

生命不息,update不止,只是此时,根节点Tile已经整装待发,来到人生的第二个状态。这里判断一下该Tile在当前状态下是否可见,如果可见,则加入到traversalQueue队列,顾名思义,这个队列就是用来遍历四叉树的。一旦traversalQueue不再为空,好戏就开始了。

image

如上,是网格调度中最关键的一部分,通过其中的if & else if & else不难看出,一共有三个逻辑:

  • screenSpaceError
    该Tile的精细度是否满足LOD要求,是否不需要请求更精细的层级。如果满足精度要求,则该Tile加入到_tilesToRender渲染队列,如果不满足精度要求,则该if判断为false

  • queueChildrenLoadAndDetermineIfChildrenAreAllRenderable
    如果之前的判断为false,则需要判断该Tile的四个子节点是否可渲染,如果可以渲染,则将子节点加入到traversalQueue中,如果不可渲染,则加入到_tileLoadQueue来下载,这两个队列中的Tile遵循各自的调度流程,此时该else if判断为false

  • else
    如果进入该else的逻辑,则说明该Tile的精度达不到要求,但其子节点还处于不可渲染状态,这时候怎么办?只要先凑合一下,把当前的Tile先放到tilesToRender队列吧,寥胜于无

这三个逻辑都比较清楚,我们先不纠结于内部具体的算法实现,以时间顺序来描述一个简化的网格调度场景:

继续从之前的根节点说起,此时根节点的数据已经加载完毕,该Tile从tileLoadQueue切换到traversalQueue队列,进入到该while循环:

首先发现该Tile0的精度不够(此时Level = 0),需要进一步的请求下一层级的切片Tile1(Level = 1的Tile),然后发现Tile的4个Children子节点还不能渲染,则把这四个节点发到tileLoadQueue(这四个节点将经历和它们父亲一样的经历,然后进入到traversalQueue队列,继续这个while循环),这样,只好先渲染这个根节点,把Tile0加入到tilesToRender渲染队列。

While循环就这样一直判断,未来的某一段时间,Level = 1的节点们各自完成了数据下载的过程,这是再进入到该while循环,就会发生一些不一样的事情了:

首先发现该Tile0的精度不够,需要进一步的请求下一层级的切片Tile1,然后发现Tile的4个Children子节点都可以渲染了,则把可渲染的这四个Tile1加入到traversalQueue,不可渲染的还和以前一样。这样,while循环会遍历traversalQueue队列中的所有Tile,轮到Tile1了,发现这次精度满足要求了,则把Tile1加入到渲染队列tilesToRender,此时,Tile0并没有进入到渲染队列,只是起到了新老接替的作用。

这是一个最简化的过程,但无论调度有多复杂,都符合如上的一个逻辑,万变不离其宗。从根本来说,在渲染和update的过程中,Cesium并不面向过程来推动整个流程,而是面向对象(状态),每一个不同状态的Tile赋予一个不同的职能,各司其职,尽其所能。这样,一个Tile从下载队列到遍历队列,最后到渲染队列,然后交出接力棒,退出渲染队列。所以这个渲染过程本质就是一个状态维护的过程,这个思想来贯彻这个Cesium的各个模块。

Tile四叉树的构建

四叉树,简单而言,就是在当前的Tile上画一个田字格,这样就形成了四个子Tile,level++,整个过程就结束了。 

Cesium里面四叉树的构建很简单,一目了然。但难能可贵的是,通过接口设计,并不需要可以的来进行这个过程,而是通过属性接口的设计方,在第一次需要getChildren的时候来构建,我觉得很巧妙。

这样避免了过多的逻辑判断,在需要Children的时候先要判断有没有,当这个链表层级多了,这个判断就很难维护了。集中在一起维护,提高代码重用,方便管理。

实现代码很简单,这里只给出代码片段,实现了一个子节点的构建,记住田字格,相信大家都能够一目了然:

image

LOD

下面涉及到LOD时关键的两个算法,第一是判断当前Tile是否满足精度的要求,如果不能满足,则需要继续请求下一级的Tile,满足渲染效果,第二是在Tile从根节点的遍历中,判断当前Tile是否可见。

  • screenSpaceError

    Cesium就是通过该函数来判断是否适可而止,停止网格的进一步剖分。算法很简单,只有下面一句话:

image

我们把公式分解一下,先看看height/sseDenominator的涵义:1 height是整个屏幕像素高,而sseDenominator是相机fovy角度的tan值的2倍,如下图所示:

image

如上图所示,根据三角函数可知:tan(fov/2) == (height/2) / far;而height/sseDenominator == (height/2) / tan(fov/2) == far;也就是相机距离屏幕中心的像素距离。

而maxGeometricError是地球赤道的周长/像素数,也就是分辨率,可以认为是在Tile不拉伸的情况下(比如一个256的Tile就是按照256的像素显示,而不是被拉伸成300的像素)一个像素代表多少米。(maxGeometricError * height) / sseDenominator == maxGeometricError * (height/sseDenominator),也就是理想情况下,相机距离屏幕中心的米单位距离,我们记作L。

而distance是当前状态下,相机距离该Tile的真实距离,于是L / distance就是一个粗略的拉伸比,如果distance值小于L,说明当前观看的位置distance比真实的位置L要近,则需要更精细的层级效果,而distance是分母,分母越小,该值就越大。换句话说,就是该值越小,说明当前的拉伸越小。

  • GlobeSurfaceTileProvider.prototype.computeTileVisibility

这个函数主要是判断当前Tile是否可见,里面主要有两个算法:

computeDistanceToTile

判断相机距离Tile的距离。一个Tile在球面上是一个弧面,在TileBoundingBox中分别记录了四个边的法向量和四个顶点的位置,这样,根据每一个边的法向量和相机距离顶点的向量,点乘的结果就是相机的垂直距离,分别计算出四个边的距离,各取东西方向(a),南北方向中的一个(b),再加上相机到地球表面的距离c,sqrt(a^2 + b^2 + c^2),则是相机距离Tile的distance。

  • computeVisibility

判断该Tile是否在裁剪面的内部,参考上图,可以看到裁剪面有六个面,则在三维中,一个Tile和其中一个面则有三种关系:内部,外部,相交。

image

如上是一个简易图,我已经在mspaint中尽力了。红色的是其中的一个平面,而箭头是它法线的方向,这里有五个圆,对应了五种BoundingSphere的情况(Cesium里面抽象了Tile,把他们简化为一个原点+半径的圆球,方便计算,这也是三维中常见的一种思路。)

首先,还是点乘,法向量和绿色直线的点乘,就是该原点距离该plane的垂直距离,这里还有一个方向的问题,也就是当角度>90时,cos值小于0.

结合上图,在x轴以上的圆心,角度小于90,点乘的结果肯定大于0,肯定在plane平面内,而在x轴以下的,角度大于90,点乘结果为负数。这时计算圆心和平面的距离,和该圆的半径比较,则可以轻松的计算出两者的关系。代码如下:

image

本篇内容大致如上考虑的篇幅,并没有在这里和大家介绍horizon occlusion这个技术,也是用于Tile裁剪的,因为只是在STK地形时采用这个方式,所以会在地形这一篇中和大家交流,简单说,效果如下(前者不采用,后者采用):

image

你会发现采用这个方式的,地球背面,不需要显示的Tile会过滤点,以往这需要计算Tile的法线和相机的角度,比较繁琐,主要是处于性能的考虑,但通过这个方式,可以快速过滤,但在数据层面需要提前准备好,这也是为什么采用STK地形的时候开启该功能的原因,不多解释,下回分解。

<think>我们正在Cesium中实现H3网格热力图。需要结合h3-js库和Cesium的图形能力。步骤:1.引入必要的库:Cesium和h3-js。2.创建Cesium Viewer。3.定义目标区域(多边形)和H3分辨率。4.使用h3.polyfill生成覆盖该区域的六边形索引数组。5.将每个六边形索引转换为地理边界(经纬度点数组)。6.在Cesium中绘制每个六边形(作为多边形图元)。7.对于热力图,我们需要数据聚合:将点数据聚合到每个六边形中,然后根据聚合值设置颜色。注意:Cesium中绘制多边形需要使用PolygonGeometry或Entity.polygon。具体实现:第一步:环境设置在Vue项目中安装cesium和h3-js。第步:初始化CesiumViewer在Vue组件中初始化Cesium Viewer。第三步:生成H3网格-定义区域(例如一个矩形)-选择合适的分辨率-使用h3.polyfill生成六边形索引数组第四步:绘制网格(无数据)-遍历每个六边形索引,获取其边界坐标(h3.h3ToGeoBoundary)-将边界坐标转换为Cesium.Cartesian3数组(注意:h3返回的是[lat, lng]数组,而Cesium需要经度在前,纬度在后,且需要将高度设为0)-使用Cesium.PolygonGeometry(或Entity)绘制每个六边形第五步:热力图数据聚合-假设我们有一组点数据(每个点有经纬度和值)-遍历每个点,使用h3.geoToH3将其转换到对应的六边形索引-对每个六边形索引内的值进行累加(或平均等)第六步:根据聚合值设置颜色-计算所有六边形聚合值的最大值(用于归一化)-定义颜色梯度函数(如从蓝色到红色)-绘制每个六边形时,根据其聚合值设置颜色第七步:性能优化-对于大量六边形,使用Primitive而不是Entity(性能更好)-使用InstanceGeometry(如果颜色固定,可以合并几何体)但每个六边形颜色不同,可能需要单独绘制代码示例:1.安装依赖:npm installh3-js cesium2. Vue组件示例(部分代码): ```vue<template><div id="cesiumContainer"></div> </template><script>import *as Cesium from'cesium';import *as h3 from'h3-js';export default{name: 'CesiumH3Heatmap',mounted() {this.initViewer();this.drawH3Heatmap();},methods: {initViewer() {//初始化CesiumViewerthis.viewer= newCesium.Viewer('cesiumContainer', {terrainProvider:Cesium.createWorldTerrain()});//定位到旧金山this.viewer.camera.flyTo({destination: Cesium.Cartesian3.fromDegrees(-122.43,37.77,5000)});},asyncdrawH3Heatmap(){//定义多边形区域(旧金山矩形)constpolygon =[[37.77, -122.43],[37.91,-122.43],[37.91, -122.23],[37.77,-122.23]];//注意:h3.polyfill要求的多边形格式是[[lat, lng],...],且要求闭合(最后一个点与第一个点相同?)但这里没有闭合,所以我们在函数中设置true表示GeoJson格式(闭合环)const resolution=9;consthexagons =h3.polyfill(polygon, resolution, true);//生成模拟点数据(用于热力图)constpoints =this.generateMockPoints(1000, polygon);//聚合数据到六边形consthexData= {};points.forEach(point=> {consthexId= h3.geoToH3(point.lat,point.lng,resolution);if(hexagons.includes(hexId)){ //只统计在区域内的点hexData[hexId]= (hexData[hexId]||0)+ point.value;}});//计算最大值const maxValue =Math.max(...Object.values(hexData));//遍历每个六边形,绘制constentities= [];hexagons.forEach(hexId=> {constvalue= hexData[hexId] ||0;//获取六边形边界(返回的是[[lat,lng], ...])constcoordinates= h3.h3ToGeoBoundary(hexId, true); //true表示GeoJson格式([lng,lat]?注意:h3ToGeoBoundary返回的是[lat,lng]数组,但GeoJson格式应该是[lng,lat]?实际上,h3ToGeoBoundary返回的是[lat,lng]数组,无论第个参数是什么,第个参数true表示返回的坐标是[[lng,lat],...]还是[lat,lng]?实际上,文档:https://h3geo.org/docs/api/indexing#h3togeoboundary//第个参数(geoJson)设为true时,返回的是[[lng, lat], [lng, lat], ...]的数组,且第一个点和最后一个点相同(闭合)//所以我们需要将其转换为Cesium的Cartesian3数组(经度,纬度,高度)constpositions =coordinates.map(coord=> Cesium.Cartesian3.fromDegrees(coord[1], coord[0],0));//创建多边形实体constcolor= this.getColor(value,maxValue);constentity =this.viewer.entities.add({polygon:{hierarchy:new Cesium.PolygonHierarchy(positions),material:color,height:0,//可以设置高度,如果在地面以上extrudedHeight:0, //设置为0表示不挤压outline:false}});entities.push(entity);});},generateMockPoints(count, polygon) {//生成在polygon范围内的随机点const[minLat, maxLat]= [Math.min(...polygon.map(p=> p[0])), Math.max(...polygon.map(p=> p[0]))];const [minLng,maxLng]= [Math.min(...polygon.map(p =>p[1])), Math.max(...polygon.map(p=> p[1]))];constpoints =[];for (let i=0;i <count;i++){constlat =minLat+ Math.random()* (maxLat- minLat);constlng= minLng+ Math.random()* (maxLng -minLng);points.push({lat,lng,value:Math.random() *100});}return points;},getColor(value,maxValue) {//根据归一化的值返回颜色constratio =value /maxValue;const r= Math.floor(255 *ratio);constb =Math.floor(255* (1 -ratio));returnnewCesium.Color(r/255,0, b/255,0.7);}} };</script> <stylescoped>#cesiumContainer{width:100%;height:100vh;} </style>```注意:-上述代码使用Entity API,对于大量六边形(比如上万个)可能会影响性能。如果性能有问题,可以改用Primitive API。-使用Primitive API绘制多边形示例:const instance= newCesium.GeometryInstance({geometry:new Cesium.PolygonGeometry({polygonHierarchy: newCesium.PolygonHierarchy(positions)}),attributes: {color: Cesium.ColorGeometryInstanceAttribute.fromColor(color)}});this.viewer.scene.primitives.add(newCesium.Primitive({geometryInstances: [instance],appearance: newCesium.PerInstanceColorAppearance({flat: true,translucent:true})}));但是,由于每个六边形颜色不同,我们需要为每个六边形创建一个GeometryInstance。这样同样会创建很多个Primitive(实际上我们可以将所有六边形放在一个Primitive中,但每个实例可以有自己的颜色属性)。我们可以使用一个Primitive包含多个GeometryInstance,每个实例有自己的颜色属性。修改绘制部分(使用Primitive): ```javascript//创建一个Primitive,包含所有六边形实例const instances= [];hexagons.forEach(hexId=> {constvalue =hexData[hexId]||0;constcoordinates =h3.h3ToGeoBoundary(hexId,true);//返回[[lng, lat],...](因为第个参数为true)constpositions =coordinates.map(coord=> Cesium.Cartesian3.fromDegrees(coord[0], coord[1],0));//注意:这里coord[0]是经度,coord[1]是纬度constcolor =this.getColor(value, maxValue);//创建实例instances.push(new Cesium.GeometryInstance({geometry: newCesium.PolygonGeometry({polygonHierarchy:new Cesium.PolygonHierarchy(positions),height:0}),attributes: {color: Cesium.ColorGeometryInstanceAttribute.fromColor(color)}}));});//添加Primitivethis.viewer.scene.primitives.add(new Cesium.Primitive({geometryInstances:instances,appearance: newCesium.PerInstanceColorAppearance({flat: true,translucent:true})}));```注意:使用Primitive时,坐标转换:h3ToGeoBoundary(hexId, true)返回的是[[lng, lat], [lng, lat], ...]的数组,所以直接使用coord[0]为经度,coord[1]为纬度。但是,注意:h3ToGeoBoundary的第个参数(geoJson)设置为true时,返回的是[ [lng, lat], [lng, lat], ...],且第一个点和最后一个点相同(形成闭合环)。所以我们在创建PolygonHierarchy时,不需要再额外添加闭合点。性能对比:使用Primitive(合并成一个)比使用多个Entity性能更好。另外,如果区域很大,六边形数量很多,可能需要考虑分批次加载或使用WebWorker生成网格。引用:在Cesium中加载网格地图的相关内容可参考专栏中的示例[^1]。相关问题:1.如何优化Cesium中大量H3六边形的渲染性能?2.如何动态更新H3热力图的数据?3.在Cesium中如何实现H3六边形的点击事件?4.如何将H3网格与Cesium的地形高程结合?5.如何实现不同分辨率H3网格的切换?</think>### 在Cesium中结合H3-JS实现网格热力图 下面是在Cesium中实现H3六边形网格热力图的完整解决方案,结合了`h3-js`的地理空间索引能力和Cesium的三维可视化能力: #### 核心实现步骤 1. **环境准备** ```bash npm install cesium h3-js ``` 2. **基础代码结构** ```javascript import * as Cesium from 'cesium'; import * as h3 from 'h3-js'; export default class H3Heatmap { constructor(viewer) { this.viewer = viewer; this.resolution = 9; // H3网格分辨率(0-15) this.dataPoints = []; // 存储热力数据点 } // 添加测试数据点 addMockData(count = 1000) { for (let i = 0; i < count; i++) { const lat = 37.77 + (Math.random() - 0.5) * 0.2; const lon = -122.43 + (Math.random() - 0.5) * 0.2; this.dataPoints.push({ position: Cesium.Cartesian3.fromDegrees(lon, lat), value: Math.floor(Math.random() * 100) }); } } // 生成热力图 generateHeatmap() { // 步骤3-6的代码将放在这里 } } ``` 3. **数据聚合到H3六边形** ```javascript // 在generateHeatmap方法中: const hexData = {}; // 将点数据聚合到H3六边形 this.dataPoints.forEach(point => { const cartographic = Cesium.Cartographic.fromCartesian(point.position); const hexId = h3.geoToH3( Cesium.Math.toDegrees(cartographic.latitude), Cesium.Math.toDegrees(cartographic.longitude), this.resolution ); if (!hexData[hexId]) { hexData[hexId] = { count: 0, sum: 0, hexId: hexId }; } hexData[hexId].count++; hexData[hexId].sum += point.value; }); ``` 4. **创建六边形几何体** ```javascript // 创建Primitive集合 const hexagonPrimitives = new Cesium.PrimitiveCollection(); Object.values(hexData).forEach(data => { const avgValue = data.sum / data.count; const boundary = h3.h3ToGeoBoundary(data.hexId, true); // 将H3边界转换为Cesium坐标 const positions = boundary.map(coord => Cesium.Cartesian3.fromDegrees(coord[0], coord[1]) ); // 创建多边形实例 const instance = new Cesium.GeometryInstance({ geometry: new Cesium.PolygonGeometry({ polygonHierarchy: new Cesium.PolygonHierarchy(positions), height: 0, perPositionHeight: false }), attributes: { color: Cesium.ColorGeometryInstanceAttribute.fromColor( this.getColorForValue(avgValue) ) } }); hexagonPrimitives.add(new Cesium.Primitive({ geometryInstances: instance, appearance: new Cesium.PerInstanceColorAppearance({ translucent: true, closed: true }) })); }); this.viewer.scene.primitives.add(hexagonPrimitives); ``` 5. **颜色映射函数** ```javascript // 根据值获取颜色 getColorForValue(value) { const maxValue = 100; // 根据实际数据调整 const ratio = Math.min(value / maxValue, 1); // 蓝 -> 黄 -> 红 渐变 return new Cesium.Color( ratio, // 红色分量 ratio * 0.5, // 绿色分量 1 - ratio, // 蓝色分量 0.7 // 透明度 ); } ``` 6. **在Vue组件中使用** ```vue <template> <div id="cesium-container"></div> </template> <script> import { onMounted } from 'vue'; import Cesium from 'cesium'; import H3Heatmap from './H3Heatmap'; export default { setup() { let viewer; let heatmap; onMounted(() => { // 初始化Cesium Viewer viewer = new Cesium.Viewer('cesium-container', { terrainProvider: Cesium.createWorldTerrain() }); // 定位到旧金山 viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(-122.43, 37.77, 5000) }); // 初始化热力图 heatmap = new H3Heatmap(viewer); heatmap.addMockData(5000); heatmap.generateHeatmap(); }); } }; </script> ``` #### 性能优化技巧 1. **合并几何体渲染** ```javascript // 使用单个Primitive渲染所有六边形 const instances = Object.values(hexData).map(data => { // ...创建GeometryInstance }); const primitive = new Cesium.Primitive({ geometryInstances: instances, appearance: new Cesium.PerInstanceColorAppearance({ translucent: true, closed: true }), asynchronous: false // 关闭异步加载提升性能 }); this.viewer.scene.primitives.add(primitive); ``` 2. **动态分辨率调整** ```javascript // 根据视图高度自动调整分辨率 this.viewer.camera.changed.addEventListener(() => { const height = this.viewer.camera.positionCartographic.height; const newRes = height > 100000 ? 7 : height > 50000 ? 8 : 9; if (newRes !== this.resolution) { this.resolution = newRes; this.generateHeatmap(); // 重新生成热力图 } }); ``` 3. **Web Worker数据处理** ```javascript // 在worker.js中处理数据聚合 addEventListener('message', (event) => { const { points, resolution } = event.data; const hexData = {}; // 聚合逻辑... postMessage(hexData); }); // 主线程中 const worker = new Worker('h3-worker.js'); worker.postMessage({ points: this.dataPoints, resolution: this.resolution }); worker.onmessage = (event) => { this.renderHexagons(event.data); // 使用返回的数据渲染 }; ``` #### 交互功能实现 1. **添加点击事件** ```javascript // 在generateHeatmap方法中 this.viewer.screenSpaceEventHandler.setInputAction((click) => { const picked = this.viewer.scene.pick(click.position); if (picked && picked.primitive) { const hexId = picked.primitive.id; const hexData = this.hexData[hexId]; // 显示信息框 this.viewer.selectedEntity = new Cesium.Entity({ position: positions[0], label: { text: `H3: ${hexId}\n值: ${hexData.avgValue.toFixed(2)}`, font: '14px sans-serif' } }); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); ``` 2. **动态更新热力图** ```javascript // 更新数据并刷新热力图 updateHeatmap(newData) { this.dataPoints = newData; this.viewer.scene.primitives.remove(this.heatmapPrimitive); this.generateHeatmap(); } ``` #### 完整示例项目 建议参考Cesium官方示例: - [Cesium + H3可视化示例](https://sandcastle.cesium.com/?src=Custom%20
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值