当前位置:   article > 正文

基于ThreeJS的3D地球_threejs 地球

threejs 地球

第一次接触threeJS,说实话,挺脑瓜子疼的!

功能:3D地球(纹理贴图),地球上添加标记点(经纬度),点击标记点弹出对应的信息框,地球入场动画,相机移动动画等。

先开效果图吧

一:添加必要的依赖

  1. yarn add three
  2. yarn add tween
  1. import * as THREE from "three"
  2. import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"
  3. import * as TWEEN from "tween"

二:组件代码

  1. <template>
  2. <div style="width: 100%; height: 100%;">
  3. <div id="layerMain">
  4. <div>{{ countryName }}</div>
  5. <div class="shape"></div>
  6. </div>
  7. <div ref="mapId" style="width: 100%; height: 100%;"></div>
  8. </div>
  9. </template>
  10. <script>
  11. import * as THREE from "three"
  12. import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"
  13. import * as TWEEN from "tween"
  14. import map_img from '../../assets/images/map.jpg'
  15. import map_wl from '../../assets/images/wl.png'
  16. let camera, scene, controls, mesh;
  17. let group = new THREE.Group();
  18. let radius = 70;
  19. let fov = 100;
  20. export default {
  21. name: 'index',
  22. data () {
  23. return {
  24. mapDom: null,
  25. renderer: null,
  26. animationType: true, // 地球入场动画
  27. rotationY: true, // 地球自动旋转
  28. meshAnimateType: false, // 标记点动画
  29. lonlat: { x: 0, y: 0, z: 200 },
  30. countryName: null, // 数据
  31. }
  32. },
  33. mounted () {
  34. this.info ()
  35. },
  36. methods: {
  37. // 初始化
  38. info () {
  39. this.infoThree ()
  40. this.infoBall ()
  41. this.infoRender ()
  42. this.renderer.domElement.addEventListener("click", this.infoMouse)
  43. },
  44. // 基本配置
  45. infoThree () {
  46. // 场景
  47. scene = new THREE.Scene()
  48. // 渲染
  49. this.renderer = new THREE.WebGLRenderer({
  50. antialias: true,
  51. })
  52. this.mapDom = this.$refs.mapId
  53. this.renderer.setSize(this.mapDom.clientWidth, this.mapDom.clientHeight)
  54. this.renderer.setClearColor(0x000, 0)
  55. this.mapDom.appendChild(this.renderer.domElement)
  56. // 相机
  57. camera = new THREE.PerspectiveCamera(
  58. fov,
  59. this.mapDom.clientWidth / this.mapDom.clientHeight,
  60. 1,
  61. 1000
  62. )
  63. camera.position.set(0, 0, 200)
  64. camera.lookAt(0, 0, 0)
  65. // 鼠标
  66. this.infoOrbitControls()
  67. },
  68. // 重新渲染
  69. infoRender() {
  70. this.renderer.clear()
  71. // 地球入场动画
  72. if (this.animationType) this.ballAnimation()
  73. // 地球旋转
  74. if (this.rotationY) this.ballRotationY()
  75. // 标记点动画
  76. if (this.meshAnimateType) this.meshAnimate()
  77. this.renderer.render(scene, camera)
  78. requestAnimationFrame(this.infoRender)
  79. TWEEN.update()
  80. },
  81. // 鼠标
  82. infoOrbitControls() {
  83. controls = new OrbitControls(camera, this.renderer.domElement)
  84. controls.enableDamping = true
  85. controls.enableZoom = true
  86. controls.autoRotate = false
  87. controls.autoRotateSpeed = 2
  88. controls.enablePan = true
  89. },
  90. // 地球
  91. infoBall() {
  92. // 纹理贴图
  93. let textureLoader = new THREE.TextureLoader();
  94. textureLoader.load(map_img, function (texture) {
  95. // 创建球
  96. let geometry = new THREE.SphereGeometry(radius, 100, 100);
  97. let material = new THREE.MeshBasicMaterial({
  98. map: texture, //设置颜色贴图属性值
  99. });
  100. //网格模型对象Mesh
  101. mesh = new THREE.Mesh(geometry, material);
  102. // 唯一标识
  103. mesh.name = "ballMain";
  104. // 添加到场景中
  105. scene.add(mesh);
  106. });
  107. },
  108. // 地球入场动画
  109. ballAnimation() {
  110. fov -= 0.6
  111. if (fov <= 45) {
  112. this.animationType = false
  113. camera.position.set(0, 0, 200)
  114. camera.lookAt(0, 0, 0)
  115. this.infoOrbitControls()
  116. } else {
  117. camera = new THREE.PerspectiveCamera(
  118. fov,
  119. this.mapDom.clientWidth / this.mapDom.clientHeight,
  120. 1,
  121. 1000
  122. );
  123. camera.position.set(0, 0, 200)
  124. camera.lookAt(0, 0, 0)
  125. }
  126. },
  127. // 地球自动旋转
  128. ballRotationY() {
  129. scene.rotation.y += 0.003
  130. },
  131. // 添加纹理标记点
  132. infoMark(item) {
  133. console.log(group)
  134. let cityGeometry = new THREE.PlaneBufferGeometry(1, 1) //默认在XOY平面上
  135. let textureLoader = new THREE.TextureLoader()
  136. let texture = textureLoader.load(map_wl)
  137. let cityWaveMaterial = new THREE.MeshBasicMaterial({
  138. color: item.color,
  139. map: texture,
  140. transparent: true,
  141. opacity: 0,
  142. side: THREE.DoubleSide
  143. })
  144. let mesh = new THREE.Mesh(cityGeometry, cityWaveMaterial)
  145. const coord = this.lon2xyz(radius * 1.01, item.lon, item.lat)
  146. mesh.scale.set(2, 2, 2)
  147. // 唯一标识
  148. mesh.name = item.name
  149. mesh.privateType = 'mark'
  150. mesh.position.set(coord.x, coord.y, coord.z)
  151. const coordVec3 = new THREE.Vector3(
  152. coord.x,
  153. coord.y,
  154. coord.z
  155. ).normalize()
  156. const meshNormal = new THREE.Vector3(0, 0, 1)
  157. mesh.quaternion.setFromUnitVectors(meshNormal, coordVec3)
  158. if (scene.getObjectByName(item.name) === undefined) {
  159. group.add(mesh)
  160. //网格模型添加到场景中
  161. scene.add(group)
  162. this.meshAnimateType = true
  163. }
  164. },
  165. // 标记点动画
  166. meshAnimate() {
  167. for (let i = 0; i < group.children.length; i++) {
  168. if (group.children[i].privateType === "mark") {
  169. // 添加初始随机数,防止动画同步
  170. group.children[i].material.opacity += Math.random() * 0.05
  171. group.children[i].scale.set(
  172. group.children[i].material.opacity + 7,
  173. group.children[i].material.opacity + 7,
  174. group.children[i].material.opacity + 7
  175. )
  176. if (group.children[i].scale.x >= 9) {
  177. group.children[i].material.opacity = 0
  178. }
  179. }
  180. }
  181. },
  182. // 移动相机
  183. cameraPos (objList) {
  184. this.frameDivClose ()
  185. let layerObj = scene.getObjectByName(objList.name)
  186. if (layerObj) {
  187. scene.rotation.y = 0
  188. this.rotationY = false
  189. new TWEEN.Tween( { x: this.lonlat.x, y: this.lonlat.y, z: this.lonlat.z } )
  190. .to( { x: layerObj.position.x * 2.8, y: layerObj.position.y * 2.8, z: layerObj.position.z * 2.8}, 1500 )
  191. .onUpdate( function () {
  192. camera.position.x = this.x
  193. camera.position.y = this.y
  194. camera.position.z = this.z
  195. camera.lookAt(0, 0, 0)
  196. })
  197. .onComplete ( ()=> {
  198. this.retrievalLayer (objList.name)
  199. })
  200. .easing(TWEEN.Easing.Sinusoidal.InOut)
  201. .start()
  202. this.lonlat = camera.position
  203. // 弹窗面板赋值
  204. this.countryName = objList.name
  205. } else {
  206. console.log('图层数据已被全部删除,请重新刷新界面,或者重新调用数据初始化方法: this.infoMap ()')
  207. alert('图层数据已被全部删除,请重新刷新界面,或者重新调用数据初始化方法: this.infoMap ()')
  208. }
  209. },
  210. // 检索指定的图层
  211. retrievalLayer (name) {
  212. let layerObj = scene.getObjectByName(name)
  213. this.infoDiv(layerObj.position.x, layerObj.position.y, layerObj.position.z)
  214. },
  215. // 鼠标事件(点击标记的点的事件)
  216. infoMouse(event) {
  217. event.preventDefault();
  218. const raycaster = new THREE.Raycaster();
  219. const mouse = new THREE.Vector2();
  220. // 通过鼠标点击位置,计算出 raycaster 所需点的位置,以屏幕为中心点,范围 -11
  221. let getBoundingClientRect = this.mapDom.getBoundingClientRect();
  222. mouse.x =
  223. ((event.clientX - getBoundingClientRect.left) /
  224. this.mapDom.offsetWidth) *
  225. 2 -
  226. 1;
  227. mouse.y =
  228. -(
  229. (event.clientY - getBoundingClientRect.top) /
  230. this.mapDom.offsetHeight
  231. ) *
  232. 2 +
  233. 1;
  234. //通过鼠标点击的位置(二维坐标)和当前相机的矩阵计算出射线位置
  235. raycaster.setFromCamera(mouse, camera);
  236. // 获取与射线相交的对象数组,其中的元素按照距离排序,越近的越靠前
  237. let intersects = raycaster.intersectObjects(scene.children);
  238. // 点击对象的处理
  239. for (let i = 0; i < intersects.length; i++) {
  240. if (intersects[i].object.name !== 'ballMain') {
  241. // 弹窗面板赋值
  242. this.countryName = intersects[i].object.name
  243. let objList = {
  244. name: intersects[i].object.name
  245. }
  246. this.cameraPos (objList)
  247. return false
  248. } else {
  249. // 开启自动旋转
  250. this.rotationY = true
  251. this.frameDivClose ()
  252. }
  253. }
  254. },
  255. // 标签
  256. infoDiv(pointx, pointy, pointz) {
  257. // 坐标转换
  258. let world_vector = new THREE.Vector3(
  259. pointx,
  260. pointy,
  261. pointz
  262. )
  263. let vector = world_vector.project(camera)
  264. let halfWidth = this.mapDom.offsetWidth / 2,
  265. halfHeight = this.mapDom.offsetHeight / 2
  266. let x = Math.round(vector.x * halfWidth + halfWidth)
  267. let y = Math.round(-vector.y * halfHeight + halfHeight)
  268. //创建div容器
  269. let moonDiv = document.getElementById("layerMain")
  270. moonDiv.style.display = "block"
  271. moonDiv.style.left = x - 150 + "px"
  272. moonDiv.style.top = y - 180 + "px"
  273. },
  274. // 关闭标签
  275. frameDivClose() {
  276. let divHtml = document.getElementById("layerMain")
  277. divHtml.style.display = "none"
  278. },
  279. // 添加光柱
  280. infoColumn (item) {
  281. const material = new THREE.MeshBasicMaterial({
  282. color: item.color,
  283. transparent: true,
  284. opacity: .9,
  285. side: THREE.DoubleSide
  286. })
  287. const coord = this.lon2xyz(radius * 1.01, item.lon, item.lat)
  288. const coordVec3 = new THREE.Vector3(coord.x, coord.y, coord.z).normalize()
  289. const geometry = new THREE.CylinderGeometry(0.2, 2.8, 30)
  290. const mesh = new THREE.Mesh(geometry, material)
  291. mesh.name = item.name
  292. mesh.privateType = 'column'
  293. mesh.position.set(coord.x, coord.y, coord.z)
  294. mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), coordVec3)
  295. group.add(mesh)
  296. scene.add(group)
  297. },
  298. // 删除所有标记点
  299. delAll () {
  300. this.frameDivClose ()
  301. group.traverse((item) => {
  302. if (item.type === 'Mesh') {
  303. item.geometry.dispose()
  304. item.material.dispose()
  305. }
  306. })
  307. scene.remove(group)
  308. // 删除group中的children
  309. if (group.children && group.children.length > 0) {
  310. let i = 0
  311. for (i; i < group.children.length; i++) {
  312. group.remove(group.children[i])
  313. }
  314. }
  315. },
  316. // 删除指定标记点
  317. delMark (item) {
  318. this.frameDivClose ()
  319. let layerObj = scene.getObjectByName(item.name)
  320. group.remove(layerObj)
  321. },
  322. // 经纬度转坐标
  323. lon2xyz(R, longitude, latitude) {
  324. const lon = (Number(longitude) + 90) * (Math.PI / 180)
  325. const lat = Number(latitude) * (Math.PI / 180)
  326. const x = R * Math.cos(lat) * Math.sin(lon)
  327. const y = R * Math.sin(lat)
  328. const z = R * Math.cos(lon) * Math.cos(lat)
  329. return { x, y, z }
  330. },
  331. }
  332. }
  333. </script>
  334. <style lang="scss">
  335. #layerMain {
  336. position: absolute;
  337. width: 300px;
  338. height: 160px;
  339. line-height: 160px;
  340. text-align: center;
  341. color: white;
  342. display: none;
  343. background-color: rgba(34,34,35,.6);
  344. .shape {
  345. position: absolute;
  346. margin: auto;
  347. left: 0;
  348. right: 0;
  349. width: 0;
  350. height: 0;
  351. bottom: -40px;
  352. border: 20px solid transparent;
  353. border-top-color: rgba(34,34,35,.6);
  354. }
  355. }
  356. </style>

三:父组件中的代码

  1. <template>
  2. <div class="home">
  3. <div class="rightMain">
  4. <div class="title">跳转操作</div>
  5. <div class="cont">
  6. <ul>
  7. <li v-for="(item, index) in objList" @click="cameraPos(item)">{{ item.name }}</li>
  8. </ul>
  9. </div>
  10. <div class="title">其他操作</div>
  11. <div class="cont">
  12. <ul>
  13. <li @click="rotationChange">{{ rotation }}</li>
  14. <li @click="columnChange">添加光柱</li>
  15. <li @click="delAllChange">删除所有</li>
  16. <li @click="delMarkChange">删除美国标记点</li>
  17. </ul>
  18. </div>
  19. <div class="title">重置操作</div>
  20. <div class="cont">
  21. <ul>
  22. <li @click="reset">初始化数据</li>
  23. </ul>
  24. </div>
  25. </div>
  26. <threeIndex ref="threeMapId"></threeIndex>
  27. </div>
  28. </template>
  29. <script>
  30. import threeIndex from '../components/three/Index'
  31. export default {
  32. name: 'HomeView',
  33. components: {
  34. threeIndex
  35. },
  36. data () {
  37. return {
  38. objList: [
  39. { lon: 116.358976, lat: 39.803282, name: "中国", color: '#1FFBC6' },
  40. { lon: 139.812263, lat: 35.677294, name: "日本", color: '#A41FE8'},
  41. { lon: 77.198596, lat: 28.575136, name: "印度", color: '#E8BB1F' },
  42. { lon: -77.02238, lat: 38.900042, name: "美国", color: '#E81F56' },
  43. { lon: 31.266092, lat: 30.085626, name: "埃及", color: '#1FFBC6' },
  44. { lon: 103.813654, lat: 1.291125, name: '新加坡', color: '#E8BB1F' },
  45. { lon: -47.930912, lat: -15.781949, name: '巴西', color: '#A41FE8' },
  46. { lon: 149.130214, lat: -35.318235, name: '澳大利亚', color: '#E81F56' }
  47. ],
  48. objList_2: [
  49. { lon: 116.358976, lat: 39.803282, name: "中国column", color: '#1FFBC6' },
  50. { lon: 139.812263, lat: 35.677294, name: "日本column", color: '#A41FE8'},
  51. { lon: 77.198596, lat: 28.575136, name: "印度column", color: '#E8BB1F' },
  52. { lon: -77.02238, lat: 38.900042, name: "美国column", color: '#E81F56' },
  53. { lon: 31.266092, lat: 30.085626, name: "埃及column", color: '#1FFBC6' },
  54. { lon: 103.813654, lat: 1.291125, name: '新加坡column', color: '#E8BB1F' },
  55. { lon: -47.930912, lat: -15.781949, name: '巴西column', color: '#A41FE8' },
  56. { lon: 149.130214, lat: -35.318235, name: '澳大利亚column', color: '#E81F56' }
  57. ],
  58. rotation: '关闭旋转'
  59. }
  60. },
  61. mounted () {
  62. this.infoMap ()
  63. },
  64. methods: {
  65. // 重置
  66. reset () {
  67. this.infoMap ()
  68. },
  69. // 地球添加标记点
  70. infoMap() {
  71. for (let i = 0; i < this.objList.length; i++) {
  72. this.$refs.threeMapId.infoMark(this.objList[i]);
  73. }
  74. },
  75. // 移动相机
  76. cameraPos(item) {
  77. this.$refs.threeMapId.cameraPos(item);
  78. },
  79. // 开启或关闭地球自动旋转
  80. rotationChange () {
  81. this.$refs.threeMapId.rotationY = !this.$refs.threeMapId.rotationY
  82. this.rotation = this.$refs.threeMapId.rotationY === true?'关闭旋转':'开启旋转'
  83. this.$refs.threeMapId.frameDivClose()
  84. },
  85. // 添加光柱infoColumn
  86. columnChange () {
  87. for (let i = 0; i < this.objList_2.length; i++) {
  88. this.$refs.threeMapId.infoColumn(this.objList_2[i]);
  89. }
  90. },
  91. // 删除所有标记点
  92. delAllChange () {
  93. for (let i = 0; i < this.objList.length; i++) {
  94. this.$refs.threeMapId.delAll(this.objList[i]);
  95. }
  96. for (let i = 0; i < this.objList_2.length; i++) {
  97. this.$refs.threeMapId.delAll(this.objList_2[i]);
  98. }
  99. },
  100. // 删除指定标记点
  101. delMarkChange () {
  102. let item = {
  103. name: '美国'
  104. }
  105. this.$refs.threeMapId.delMark(item)
  106. },
  107. }
  108. }
  109. </script>
  110. <style lang="scss">
  111. .home {
  112. position: relative;
  113. width: 100%;
  114. height: 100%;
  115. overflow: hidden;
  116. background-image: url("../assets/images/back.png");
  117. background-size: 100% 130%;
  118. .rightMain {
  119. position: absolute;
  120. right: 0;
  121. width: 300px;
  122. height: 100%;
  123. z-index: 100;
  124. padding: 10px;
  125. box-sizing: border-box;
  126. color: white;
  127. background-color: rgba(255,255,255, .2);
  128. .title {
  129. width: 100%;
  130. font-size: 18px;
  131. font-weight: bold;
  132. margin-bottom: 10px;
  133. }
  134. .cont {
  135. height: 150px;
  136. ul {
  137. padding: 0;
  138. margin-bottom: 0;
  139. li {
  140. list-style: none;
  141. float: left;
  142. width: 33.33%;
  143. padding: 10px 0;
  144. text-align: center;
  145. cursor: pointer;
  146. font-size: 14px;
  147. &:hover {
  148. color: aquamarine;
  149. }
  150. }
  151. }
  152. }
  153. }
  154. }
  155. </style>

四:项目gitee地址

mythree: 基于three的3D地球案例

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/665472
推荐阅读
相关标签
  

闽ICP备14008679号