旋转3D地球效果如下 :
整体思路:
- 加载世界地图坐标信息json数据
- 准备生成散点、铁路线、箭头标注、文字标注等一些echarts需要的配置对象
- 使用加载的json数据生成地图基础纹理,基本纹理中series包含第二步骤生成的那些东西
- 使用globe配置对象绘制3d地球,在globe里面配置生成的纹理
- 可开启一个定时器,让地球旋转起来
1.加载世界地图坐标信息json数据
javascript
// request.js
//没有基地址 访问根目录下文件
export const GETNOBASE = async (url, params) => {
try {
const data = await axios.get(url, {
params: params,
});
return data;
} catch (error) {
return error;
}
}
// earth3d.vue
// 加载地理信息
async loadGeoJson() {
const res = await GETNOBASE("./map-geojson/worldZh.json");
return res.data;
}
// worldZh.json这个文件自己去这个网站下 [数据可视化平台](https://datav.aliyun.com/portal/school/atlas/area_selector)
// worldZh.json这个文件我放在public/map-geojson文件下的
- 准备生成散点、铁路线、箭头标注、文字标注等一些echarts需要的配置对象
javascript
// 准备绘制数据
// earth3d.vue
prepareData() {
this.prepareEffectScatterData();
this.prepareTrainLines();
this.prepareArrows();
this.prepareTrainNames();
}
// 生成散点数据
prepareEffectScatterData() {
cityData.forEach((point) => {
this.effectScatter.push({
name: point.name,
value: point.value,
symbol: point.hasIcon ? `image://${point.icon}` : "circle", // 使用自定义图片作为symbol
symbolSize: point.name === "重庆" ? 12 : 8,
z: point.name === "重庆" ? 5 : 2,
label: {
position: point.position,
fontSize: point.name === "重庆" ? 16 : 14,
// fontWeight: 'bold',
color: point.name === "重庆" ? "#e51c1c" : "#fff",
},
});
});
}
// 生成铁路线数据
prepareTrainLines() {
this.trainLines = [
drawTrainLine(hasTielu, 4, "solid", "rgba(255, 255, 255, 1)", 3),
drawTrainLine(hasTielu, 4, [10, 10], "#000", 2),
];
}
// 生成箭头标注
prepareArrows() {
this.arrows = [
createArrow({
name: "向上的箭头",
value: [102.3043, 40.2248],
x: 200,
y: 250,
type: "up",
}),
createArrow({
name: "向左的箭头",
value: [71.511721, 42.302711],
x: 300,
y: 150,
type: "left",
}),
createArrow({
name: "向下的箭头",
value: [99.36, 17.58],
x: 150,
y: 300,
type: "down",
}),
];
}
// 生成文字标注
prepareTrainNames() {
this.trainNames = [
...trainName.map((item) => createNamePoint(item)), // 印在地球上不带框框的文字
createNamePointImg({ name: "勒是铁路线", value: [102.11293, 42.14693] }), // 带框框的提示文字
];
}
// map.js
// 散点数据
export const cityData = [
{
name: '科伦坡',
value: [79.52, 6.55],
position: 'left', // 这个是在点的四周位置,top,right,left,bottom,根据实际情况来定
hasIcon: false,
},
{
name: "钦州",
value: [107.27, 21.25], // [107.27, 21.35]
position: 'bottom',
hasIcon: false,
},
{
name: "重庆",
value: [106.55116, 29.61203],
position: 'bottom',
hasIcon: true,
icon: cqIcon, // 这个是引入的图片资源
},
{
name: "武汉",
value: [113.41, 29.58],
position: 'top',
hasIcon: false,
},
{
name: "上海",
value: [121.451830, 31.175201],
position: 'top',
hasIcon: false,
},
{
name: "成都",
value: [104.083736, 30.65318],
position: 'top',
hasIcon: false,
},
{
name: "二连浩特",
value: [111.982513, 43.655688],
position: 'right',
hasIcon: false,
},
{
name: "马拉",
value: [19.40624, 52.12210],
position: 'bottom',
hasIcon: false,
},
{
name: "明斯克",
value: [27.30, 53.51],
position: 'bottom',
hasIcon: false,
},
{
name: "吉大港",
value: [91.48, 22.18],
position: 'top',
hasIcon: false,
},
{
name: "台湾",
value: [120.1, 21.45],
position: 'right',
hasIcon: false,
},
{
name: "满洲里",
value: [117.3837, 49.6035],
position: 'right',
hasIcon: false,
},
{
name: "霍尔果斯",
value: [80.4852, 44.163962],
position: 'top',
hasIcon: false,
},
{
name: "莫克兰",
value: [62.280, 25.2200],
position: 'right',
hasIcon: false,
},
{
name: "塔什干",
value: [69.2444, 41.3396],
position: 'bottom',
hasIcon: false,
},
{
name: "吉洪诺沃",
value: [40.2936, 56.3469],
position: 'top',
hasIcon: false,
},
]
// 铁路线
export const hasTielu = [
{
name: "重庆-成都",
coords: [
[106.55116, 29.61203],
[104.083736, 30.65318],
],
lineStyle: { curveness: -0.1 },
},
{
name: "成都-太原",
coords: [
[104.083736, 30.65318],
[112.510097, 37.936464],
],
lineStyle: { curveness: -0.1 },
},
{
name: "太原-二连浩特",
coords: [
[112.510097, 37.936464],
[111.982513, 43.655688],
],
lineStyle: { curveness: -0.1 },
},
{
name: "二连浩特-a",
coords: [
[111.982513, 43.655688],
[102.3043, 51.2248],
],
lineStyle: { curveness: -0.2 },
},
{
name: "a-新西伯利亚州",
coords: [
[102.3043, 51.2248],
[79.733157, 55.566475],
],
lineStyle: { curveness: -0.1 },
},
{
name: "新西伯利亚州-吉洪诺沃",
coords: [
[79.733157, 55.566475],
[40.2936, 56.3469],
],
lineStyle: { curveness: -0.1 },
},
{
name: "吉洪诺沃-明斯克",
coords: [
[40.2936, 56.3469],
[27.30, 53.51],
],
lineStyle: { curveness: 0 },
},
{
name: "重庆-荆州",
coords: [
[106.55116, 29.61203],
[111.15, 29.26],
],
lineStyle: { curveness: -0.1 },
},
{
name: "荆州-武汉",
coords: [
[111.15, 29.26],
[113.41, 29.58],
],
lineStyle: { curveness: 0 },
},
{
name: "武汉-上海",
coords: [
[113.41, 29.58],
[121.451830, 31.175201],
],
lineStyle: { curveness: 0.1 },
},
{
name: "成都-b",
coords: [
[104.083736, 30.65318],
[95.51293, 37.44693],
],
lineStyle: { curveness: -0.1 },
},
{
name: "b-霍尔果斯",
coords: [
[95.51293, 37.44693],
[80.4852, 44.163962],
],
lineStyle: { curveness: -0.1 },
},
{
name: "霍尔果斯-塔什干",
coords: [
[80.4852, 44.163962],
[69.2444, 41.3396],
],
lineStyle: { curveness: 0.1 },
},
{
name: "塔什干-阿克套",
coords: [
[69.2444, 41.3396],
[51.1644, 43.6595],
],
lineStyle: { curveness: 0.1 },
},
{
name: "明斯克-马拉",
coords: [
[27.30, 53.51],
[19.40624, 52.12210],
],
lineStyle: { curveness: 0 },
},
]
// 地球上无框文字
export const trainName = [
{
name: '白色无框文字',
value: [55.1644, 44.6595],
position: 'top',
color: '#fff',
rotate: 8,
},
{
name: '深蓝无框文字',
value: [90.774369, 50.14839],
position: 'top',
color: '#214D97',
rotate: -10,
},
{
name: '深蓝无框文字',
value: [71.5598, 29.228952],
position: 'top',
color: '#214D97',
rotate: 50,
},
{
name: '深蓝无框文字',
value: [81.538926, 18.785731],
position: 'top',
color: '#214D97',
rotate: 45,
},
]
// 飞线--线段
export const airLine = [
{
value: (Math.random() * 3000).toFixed(2),
coords: [
[106.55116, 29.61203], // 重庆
[91.48, 22.18], // 吉大港
]
},
{
value: (Math.random() * 3000).toFixed(2),
coords: [
[91.48, 22.18], // 吉大港
[79.52, 6.55], // 科伦坡
]
},
]
// mapFun.js
// 画铁路线
export const drawTrainLine = (arr, zIndex, lineType, lineColor, lineW) => {
return {
name: "铁路线",
type: "lines",
coordinateSystem: "geo",
zlevel: zIndex,
effect: {
show: true,
period: 6, // 动画周期
trailLength: 0,
color: "#7FFBFD", // 轨迹颜色
symbol: `image://${trainIcon}`, // 使用自定义图片作为symbol
symbolSize: 10,
},
lineStyle: {
normal: {
type: lineType, // solid,dotted,dashed
color: lineColor || "#fff",
width: lineW || 3,
opacity: 1,
curveness: 0.3,
},
},
data: arr.map((line) => ({
coords: line.coords, // 起点和终点
lineStyle: {
normal: {
curveness: line.lineStyle.curveness
},
},
})),
}
}
// 渲染大箭头标注
const imgMap = { // 全是自定义图片
up: arrow1,
left: arrow2,
down: arrow3,
}
export const createArrow = (obj) => {
return {
name: obj.name,
type: 'scatter',
coordinateSystem: 'geo',
symbol: `image://${imgMap[obj.type]}`, // 使用自定义图片作为symbol
symbolSize: [obj.x, obj.y], // 设置图标大小
label: {
show: false,
position: 'inside', // 文字显示在图标内部
formatter: '{b}', // 显示标注点的名字
color: '#fff', // 文字颜色
},
data: [
{
name: obj.name, // 图标上显示的文字
value: obj.value, // 标注的地理坐标(经纬度)
},
],
zlevel: 2,
}
}
// 文字标注名称,利用散点图画出来
export const createNamePoint = (point) => {
return {
name: point.name,
type: 'scatter',
coordinateSystem: "geo",
data: [{ name: point.name, value: point.value }],
symbol: "circle",
symbolSize: 0, // 不显示实际的点,只显示文字
zlevel: 5,
label: {
show: true,
formatter: '{b}', // {b} 会显示数据名称
position: point.position,
fontSize: 10, // getFontSize()
fontWeight: 'bold',
color: point.color || "#fff",
rotate: point.rotate,
}
}
}
// 带框的文字标注点,有自定义的图片
export const createNamePointImg = (obj) => {
return {
name: obj.name,
type: 'scatter',
coordinateSystem: 'geo',
symbol: `image://${imageURL}`, // 使用自定义图片作为symbol
symbolSize: [120, 50], // 设置图标大小
label: {
show: true,
position: 'inside', // 文字显示在图标内部
formatter: '{b}', // 显示标注点的名字
color: '#fff', // 文字颜色
fontSize: 10, // 文字大小
offset: [5, -10],
},
data: [
{
name: obj.name, // 图标上显示的文字
value: obj.value, // 标注的地理坐标(经纬度)
},
],
zlevel: 7,
}
}
3.使用加载的json数据生成地图基础纹理,基本纹理中series包含第二步骤生成的那些东西
javascript
// earth3d.vue
// 生成基础纹理
generateBaseTexture(geoJson) {
// 注册geo数据
echarts.registerMap(this.code, geoJson);
// 创建画布 生成纹理
const canvas = document.createElement("canvas");
this.baseTexture = echarts.init(canvas, null, { width: 1920, height: 1080 });
this.baseTexture.setOption({
backgroundColor: "rgba(26, 40, 71, 0.85)", //相当于海洋颜色
geo: {
type: "map",
map: this.code,
left: 0,
top: 0,
right: 0,
bottom: 0,
boundingCoords: [
[-180, 90],
[180, -90],
],
roam: false,
selectedMode: "single",
select: {
itemStyle: {
areaColor: "#3ADAF4",
},
label: {
show: true,
color: "#000",
fontSize: 10,
},
},
emphasis: {
disabled: true,
itemStyle: {
areaColor: "#3ADAF4",
},
label: {
show: true,
color: "#000",
fontSize: 10,
},
},
itemStyle: {
areaColor: "#2C89F5", // #1EA3C8 rgba(44, 153, 245, 1)
borderColor: "#314E85",
},
},
series: [
// 散点
{
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 6,
rippleEffect: { number: 0, brushType: "stroke" },
label: { show: true, formatter: "{b}", distance: 5 },
itemStyle: { normal: { color: "#fdf80c", borderColor: "#fff" } },
data: this.effectScatter,
},
// 飞线
{
type: "lines",
zlevel: 3,
effect: {
show: true,
period: 4,
trailLength: 0,
symbol: "arrow",
symbolSize: 12,
},
lineStyle: {
normal: {
color: "#fdf80c",
width: 2,
type: "solid",
opacity: 1,
curveness: -0.1,
},
},
data: airLine,
},
...this.trainLines,
...this.trainNames,
...this.arrows,
],
});
// 监听鼠标悬浮和点击事件
this.baseTexture.on("click", (params) => {
if (params.name === "重庆") {
// 点击了重庆,要干点什么
}
});
}
4.使用globe配置对象绘制3d地球,在globe里面配置生成的纹理
javascript
// earth3d.vue
// 绘制地球
drawEarth() {
const option = {
globe: {
baseTexture: this.baseTexture, // 基础纹理
// globeRadius: getEarthRadius(),
shading: "color", // color lambert // 'lambert' 通过经典的 lambert 着色表现光照带来的明暗
light: {
ambient: { intensity: 0.9 },
main: { alpha: -45, beta: 45, intensity: 0.6 }, // 主光源
},
atmosphere: {
show: true,
color: "rgba(33, 97, 179, 0.6)",
glowPower: 4,
},
viewControl: {
projection: 'perspective',
alpha: this.currentAlpha,
beta: this.currentBeta,
autoRotateSpeed: 0.6,
autoRotate: true, // 开启自动旋转 true
autoRotateAfterStill: 5, //鼠标停止操纵后,恢复自转时间
// distance: 200, //默认视角距离主体距离
distance: this.currentDistance, // 使用记录的距离
minDistance: 40, // 最小视角距离
maxDistance: 400, // 最大视角距离
damping: 0, //鼠标旋转或缩放操作时的迟滞因子
rotateSensitivity: 0.8, //旋转操作的灵敏度
zoomSensitivity: 8, //缩放操作的灵敏度
maxBeta: this.maxBeta,
},
layers: [
{
show: true,
type: "blend",
blendTo: "emission",
texture: this.createLatitudeLongitudeGrid(),
// distance: getEarthRadius() + 5, // 网格稍微在地球表面上方
distance: this.currentDistance + 5,
},
],
top: "6%",
},
series: [],
};
this.myChart.clear();
this.myChart.setOption(option);
}
// 创建经纬网格
createLatitudeLongitudeGrid() {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const size = 1024;
canvas.width = size;
canvas.height = size;
ctx.strokeStyle = "#114A72";
ctx.lineWidth = 1;
ctx.globalAlpha = 0.8;
// 绘制经线
for (let i = 0; i <= size; i += size / 18) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, size);
ctx.stroke();
}
// 绘制纬线
for (let j = 0; j <= size; j += size / 9) {
ctx.beginPath();
ctx.moveTo(0, j);
ctx.lineTo(size, j);
ctx.stroke();
}
console.log(canvas);
return canvas;
}
5.可开启一个定时器,让地球旋转起来
javascript
setTimer() {
this.rotateTimer = setInterval(() => {
this.initBeta++;
if (this.initBeta > this.maxBeta) {
clearInterval(this.rotateTimer);
this.initBeta = 180;
this.currentBeta = this.initBeta
this.drawEarth();
this.setTimer();
}
}, 1000);
}
完整的代码:
javascript
// earth3d.vue
<template>
<div class="earth-3d" ref="earth3dRef" id="earth3dRef"></div>
</template>
<script>
import * as echarts from "echarts";
import "echarts-gl"; // 引入 ECharts 3D 的模块 必须引入 ECharts GL 才能使用 WebGL 功能
import { GETNOBASE } from "api/request";
import { airLine, cityData, hasTielu, trainName } from "./js/map";
import {
drawTrainLine,
createNamePoint,
createNamePointImg,
createArrow,
} from "./js/echartsFun";
export default {
data() {
return {
myChart: null,
baseTexture: null,
code: "world",
effectScatter: [],
trainLines: [], // 铁路线集合
arrows: [], // 箭头集合
trainNames: [], // 文字标注集合
maxBeta: 220, // 设置旋转的最大角度
rotateTimer: null,
initBeta: 180,
currentDistance: 230, // 添加当前视角距离属性
currentAlpha: 30, // 添加当前视角角度属性
currentBeta: 180, // 添加当前视角角度属性
};
},
mounted() {
if (!this.webglSupport()) {
this.message("您的浏览器不支持 WebGL,请切换或升级浏览器");
return;
}
this.$nextTick(() => {
this.myChart = echarts.init(this.$refs.earth3dRef);
this.initializeMap();
});
},
beforeDestroy() {
if (this.myChart) {
this.myChart.dispose();
}
if (this.rotateTimer) {
clearInterval(this.rotateTimer);
this.initBeta = 180;
}
},
methods: {
// 这段代码会在组件挂载时检查用户的浏览器是否支持 WebGL。如果不支持,会给出提示信息,而不会继续执行图表初始化
webglSupport() {
try {
const canvas = document.createElement("canvas");
return !!(
window.WebGLRenderingContext &&
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
);
} catch (e) {
return false;
}
},
// 初始化地图
initializeMap() {
this.loadGeoJson().then((geoJson) => {
this.prepareData();
this.generateBaseTexture(geoJson);
this.drawEarth();
this.setTimer();
// 获取地图容器元素
const mapContainer = this.$refs.earth3dRef;
// 添加滚轮事件监听
mapContainer.addEventListener('wheel', () => {
// 获取当前视图的配置
const viewControl = this.myChart.getOption().globe[0].viewControl;
this.currentDistance = viewControl.distance;
});
});
},
setTimer() {
this.rotateTimer = setInterval(() => {
this.initBeta++;
if (this.initBeta > this.maxBeta) {
clearInterval(this.rotateTimer);
this.initBeta = 180;
this.currentBeta = this.initBeta
this.drawEarth();
this.setTimer();
}
}, 1000);
},
// 加载地理信息
async loadGeoJson() {
const res = await GETNOBASE("./map-geojson/worldZh.json");
return res.data;
},
// 准备绘制数据
prepareData() {
this.prepareEffectScatterData();
this.prepareTrainLines();
this.prepareArrows();
this.prepareTrainNames();
},
// 生成散点数据
prepareEffectScatterData() {
cityData.forEach((point) => {
this.effectScatter.push({
name: point.name,
value: point.value,
symbol: point.hasIcon ? `image://${point.icon}` : "circle", // 使用自定义图片作为symbol
symbolSize: point.name === "重庆" ? 12 : 8,
z: point.name === "重庆" ? 5 : 2,
label: {
position: point.position,
fontSize: point.name === "重庆" ? 16 : 14,
// fontWeight: 'bold',
color: point.name === "重庆" ? "#e51c1c" : "#fff",
},
});
});
},
// 生成铁路线数据
prepareTrainLines() {
this.trainLines = [
drawTrainLine(hasTielu, 4, "solid", "rgba(255, 255, 255, 1)", 3),
drawTrainLine(hasTielu, 4, [10, 10], "#000", 2),
];
},
// 生成箭头标注
prepareArrows() {
this.arrows = [
createArrow({
name: "向上的箭头",
value: [102.3043, 40.2248],
x: 20,
y: 25,
type: "up",
}),
createArrow({
name: "向左的箭头",
value: [71.511721, 42.302711],
x: 30,
y: 15,
type: "left",
}),
createArrow({
name: "向下的箭头",
value: [99.36, 17.58],
x: 15,
y: 30,
type: "down",
}),
];
},
// 生成文字标注
prepareTrainNames() {
this.trainNames = [
...trainName.map((item) => createNamePoint(item)), // 印在地球上不带框框的文字
createNamePointImg({ name: "勒是铁路线", value: [102.11293, 42.14693] }), // 带框框的提示文字
];
},
// 生成基础纹理
generateBaseTexture(geoJson) {
// 注册geo数据
echarts.registerMap(this.code, geoJson);
// 创建画布 生成纹理
const canvas = document.createElement("canvas");
this.baseTexture = echarts.init(canvas, null, { width: 1920, height: 1080 });
this.baseTexture.setOption({
backgroundColor: "rgba(26, 40, 71, 0.85)", //相当于海洋颜色
geo: {
type: "map",
map: this.code,
left: 0,
top: 0,
right: 0,
bottom: 0,
boundingCoords: [
[-180, 90],
[180, -90],
],
roam: false,
selectedMode: "single",
select: {
itemStyle: {
areaColor: "#3ADAF4",
},
label: {
show: true,
color: "#000",
fontSize: 10,
},
},
emphasis: {
disabled: true,
itemStyle: {
areaColor: "#3ADAF4",
},
label: {
show: true,
color: "#000",
fontSize: 10,
},
},
itemStyle: {
areaColor: "#2C89F5", // #1EA3C8 rgba(44, 153, 245, 1)
borderColor: "#314E85",
},
},
series: [
// 散点
{
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 6,
rippleEffect: { number: 0, brushType: "stroke" },
label: { show: true, formatter: "{b}", distance: 5 },
itemStyle: { normal: { color: "#fdf80c", borderColor: "#fff" } },
data: this.effectScatter,
},
// 飞线
{
type: "lines",
zlevel: 3,
effect: {
show: true,
period: 4,
trailLength: 0,
symbol: "arrow",
symbolSize: 12,
},
lineStyle: {
normal: {
color: "#fdf80c",
width: 2,
type: "solid",
opacity: 1,
curveness: -0.1,
},
},
data: airLine,
},
...this.trainLines,
...this.trainNames,
...this.arrows,
],
});
// 监听鼠标悬浮和点击事件
this.baseTexture.on("click", (params) => {
if (params.name === "重庆") {
// 点击了重庆,要干点什么
}
});
},
// 绘制地球
drawEarth() {
const option = {
globe: {
baseTexture: this.baseTexture, // 基础纹理
shading: "color", // color lambert // 'lambert' 通过经典的 lambert 着色表现光照带来的明暗
light: {
ambient: { intensity: 0.9 },
main: { alpha: -45, beta: 45, intensity: 0.6 }, // 主光源
},
atmosphere: {// 利用大气层实现发光效果
show: true,
color: "rgba(33, 97, 179, 0.6)",
glowPower: 4,
},
viewControl: {
projection: 'perspective',
alpha: this.currentAlpha,
beta: this.currentBeta,
autoRotateSpeed: 0.6,
autoRotate: true, // 开启自动旋转 true
autoRotateAfterStill: 5, //鼠标停止操纵后,恢复自转时间
// distance: 200, //默认视角距离主体距离
distance: this.currentDistance, // 使用记录的距离
minDistance: 40, // 最小视角距离
maxDistance: 400, // 最大视角距离
damping: 0, //鼠标旋转或缩放操作时的迟滞因子
rotateSensitivity: 0.8, //旋转操作的灵敏度
zoomSensitivity: 8, //缩放操作的灵敏度
maxBeta: this.maxBeta,
},
layers: [
{
show: true,
type: "blend",
blendTo: "emission",
texture: this.createLatitudeLongitudeGrid(),
// distance: getEarthRadius() + 5, // 网格稍微在地球表面上方
distance: this.currentDistance + 5,
},
],
top: "6%",
},
series: [],
};
this.myChart.clear();
this.myChart.setOption(option);
},
// 创建经纬网格
createLatitudeLongitudeGrid() {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const size = 1024;
canvas.width = size;
canvas.height = size;
ctx.strokeStyle = "#114A72";
ctx.lineWidth = 1;
ctx.globalAlpha = 0.8;
// 绘制经线
for (let i = 0; i <= size; i += size / 18) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, size);
ctx.stroke();
}
// 绘制纬线
for (let j = 0; j <= size; j += size / 9) {
ctx.beginPath();
ctx.moveTo(0, j);
ctx.lineTo(size, j);
ctx.stroke();
}
return canvas;
},
// 消息提示
message(text) {
this.$Message({
text: text,
type: "warning",
});
},
},
};
</script>
<style lang="scss" scoped>
.earth-3d {
width: 100%;
height: 100%;
}
</style>
现在你已经拥有 了一个可以旋转的3d地球了