24 使用Three.js实现炫酷的赛博朋克风格3D数字地球大屏 - AnnGreen1/article GitHub Wiki

使用Three.js实现炫酷的赛博朋克风格3D数字地球大屏

mp.weixin.qq.comdragonir 前端Q

点击上方前端Q,关注公众号-

回复加群,加入前端Q技术交流群

声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。

背景

近期工作有涉及到数字大屏的需求,于是利用业余时间,结合 Three.js 和 CSS实现赛博朋克2077风格视觉效果[2] 实现炫酷 3D 数字地球大屏页面。页面使用 React + Three.js + Echarts + stylus 技术栈,本文涉及到的主要知识点包括:THREE.Spherical 球体坐标系的应用、Shader 结合 TWEEN 实现飞线和冲击波动画效果、dat.GUI 调试工具库的使用、clip-path 创建不规则图形、Echarts 的基本使用方法、radial-gradient 创建雷达图形及动画、GlitchPass 添加故障风格后期、Raycaster 网格点击事件等。

效果

如下图 👇 所示,页面主要头部、两侧卡片、底部仪表盘以及主体 3D 地球 🌐 构成,地球外围有 飞线 动画和 冲击波 动画效果 🌠 ,通过 🖱 鼠标可以旋转和放大地球。点击第一张卡片的 START 按钮会给页面添加故障风格后期 双击地球会弹出随机提示语弹窗。

  • 💻 本页面仅适配 PC 端,大屏访问效果更佳。

  • 👁‍🗨 在线预览地址1:3d-eosin.vercel.app/#/earthDigi…[3]

  • 👁‍🗨 在线预览地址2:dragonir.github.io/3d/#/earthD…[4]

码上掘金

实现

📦 资源引入

引入开发必备的资源,其中除了基础的 React 和样式表之外,dat.gui 用于动态控制页面参数,其他剩余的主要分为两部分:Three.js相关, OrbitControls 用于镜头轨道控制、TWEEN 用于补间动画控制、mergeBufferGeometries 用户合并模型、EffectComposer RenderPass GlitchPass 用于生成后期故障效果动画、 lineFragmentShader 是飞线的 ShaderEcharts相关按需引入需要的组件,最后使用 echarts.use 使其生效。

import'./index.styl';importReactfrom'react';import*asdatfrom'dat.gui';//three.js相关import*asTHREEfrom'three';import{OrbitControls}from'three/examples/jsm/controls/OrbitControls';import{TWEEN}from'three/examples/jsm/libs/tween.module.min.js';import{mergeBufferGeometries}from'three/examples/jsm/utils/BufferGeometryUtils';import{EffectComposer}from'three/examples/jsm/postprocessing/EffectComposer.js';import{RenderPass}from'three/examples/jsm/postprocessing/RenderPass.js';import{GlitchPass}from'three/examples/jsm/postprocessing/GlitchPass.js';importlineFragmentShaderfrom'@/containers/EarthDigital/shaders/line/fragment.glsl';//echarts相关import*asechartsfrom'echarts/core';import{BarChart/*...*/}from'echarts/charts';import{GridComponent/*...*/}from'echarts/components';import{LabelLayout/*...*/}from'echarts/features';import{CanvasRenderer}from'echarts/renderers';echarts.use([BarChart,GridComponent,/*...*/]);复制代码

📃 页面结构

页面主要结构如以下代码所示,.webgl 用于渲染 3D 数字地球;.header 是页面顶部,里面包括时间日期星际坐标Cyberpunk 2077 Logo、本人 Github 仓库地址等;.aside 是左右两侧的图表展示区域;.footer 是底部的仪表盘,展示一些雷达动画和文本信息;如果仔细观察,可以看出背景有噪点效果,.bg 就是用于生成噪点背景效果。

<divclassName='earth_digital'><canvasclassName='webgl'></canvas><headerclassName='hudheader'><header></header><asideclassName='hudasideleft'></aside><asideclassName='hudasideright'></aside><footerclassName='hudfooter'></footer><sectionclassName="bg"></section></div>复制代码

🔩 场景初始化

定义一些全局变量和参数,初始化场景相机镜头轨道控制器页面缩放监听、添加页面重绘更新动画等进行场景初始化。

constrenderer=newTHREE.WebGLRenderer({canvas:document.querySelector('canvas.webgl'),antialias:true,alpha:true});renderer.setSize(window.innerWidth,window.innerHeight);renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));//创建场景constscene=newTHREE.Scene();//创建相机constcamera=newTHREE.PerspectiveCamera(45,window.innerWidth/window.innerHeight,.01,50);camera.position.set(0,0,15.5);//添加镜头轨道控制器constcontrols=newOrbitControls(camera,renderer.domElement);controls.enableDamping=true;controls.enablePan=false;//页面缩放监听并重新更新场景和相机window.addEventListener('resize',()=>{camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);},false);//页面重绘动画renderer.setAnimationLoop(_=>{TWEEN.update();earth.rotation.y+=0.001;renderer.render(scene,camera);});复制代码

🌐 创建点状地球

具体思路是使用 THREE.Spherical 创建一个球体坐标系 ,然后创建 10000 个平面网格圆点,将它们的空间坐标转换成球坐标,并使用 mergeBufferGeometries 将它们合并为一个网格。然后使用一张如下图所示的地图图片作为材质,在 shader 中根据材质图片的颜色分布调整圆点的大小和透明度,根据传入的参数调整圆点的颜色和大小比例。然后创建一个球体 SphereGeometry,使用生成的着色器材质,并将它添加到场景中。到此,一个点状地球 🌐 模型就完成了,具体实现如下。

图片

//创建球类坐标letsph=newTHREE.Spherical();letdummyObj=newTHREE.Object3D();letp=newTHREE.Vector3();letgeoms=[],rad=5,r=0;letdlong=Math.PI*(3-Math.sqrt(5));letdz=2/counter;letlong=0;letz=1-dz/2;letparams={colors:{base:'#f9f002',gradInner:'#8ae66e',gradOuter:'#03c03c'},reset:()=>{controls.reset()}}letuniforms={impacts:{value:impacts},//陆地色块大小maxSize:{value:.04},//海洋色块大小minSize:{value:.025},//冲击波高度waveHeight:{value:.1},//冲击波范围scaling:{value:1},//冲击波径向渐变内侧颜色gradInner:{value:newTHREE.Color(params.colors.gradInner)},//冲击波径向渐变外侧颜色gradOuter:{value:newTHREE.Color(params.colors.gradOuter)}}//创建10000个平面圆点网格并将其定位到球坐标for(leti=0;i<10000;i++){r=Math.sqrt(1-z*z);p.set(Math.cos(long)*r,z,-Math.sin(long)*r).multiplyScalar(rad);z=z-dz;long=long+dlong;sph.setFromVector3(p);dummyObj.lookAt(p);dummyObj.updateMatrix();letg=newTHREE.PlaneGeometry(1,1);g.applyMatrix4(dummyObj.matrix);g.translate(p.x,p.y,p.z);letcenters=[p.x,p.y,p.z,p.x,p.y,p.z,p.x,p.y,p.z,p.x,p.y,p.z];letuv=newTHREE.Vector2((sph.theta+Math.PI)/(Math.PI*2),1.-sph.phi/Math.PI);letuvs=[uv.x,uv.y,uv.x,uv.y,uv.x,uv.y,uv.x,uv.y];g.setAttribute('center',newTHREE.Float32BufferAttribute(centers,3));g.setAttribute('baseUv',newTHREE.Float32BufferAttribute(uvs,2));geoms.push(g);}//将多个网格合并为一个网格letg=mergeBufferGeometries(geoms);letm=newTHREE.MeshBasicMaterial({color:newTHREE.Color(params.colors.base),onBeforeCompile:shader=>{shader.uniforms.impacts=uniforms.impacts;shader.uniforms.maxSize=uniforms.maxSize;shader.uniforms.minSize=uniforms.minSize;shader.uniforms.waveHeight=uniforms.waveHeight;shader.uniforms.scaling=uniforms.scaling;shader.uniforms.gradInner=uniforms.gradInner;shader.uniforms.gradOuter=uniforms.gradOuter;//将地球图片作为参数传递给shadershader.uniforms.tex={value:newTHREE.TextureLoader().load(imgData)};shader.vertexShader=vertexShader;shader.fragmentShader=fragmentShader;);}});//创建球体constearth=newTHREE.Mesh(g,m);earth.rotation.y=Math.PI;earth.add(newTHREE.Mesh(newTHREE.SphereGeometry(4.9995,72,36),newTHREE.MeshBasicMaterial({color:newTHREE.Color(0x000000)})));earth.position.set(0,-.4,0);scene.add(earth);复制代码

🔧 添加调试工具

为了实时调整球体的样式和后续飞线和冲击波的参数调整,可以使用工具库 dat.GUI。它可以创建一个表单添加到页面,通过调整表单上面的参数、滑块和数值等方式绑定页面参数,参数值更改后可以实时更新画面,这样就不用一边到编辑器调整代码一边到浏览器查看效果了。基本用法如下,本例中可以在页面通过点击键盘 H键显示或隐藏参数表单,通过表单可以修改 🌐 地球背景色、飞线颜色、冲击波幅度大小等效果。

constgui=newdat.GUI();gui.add(uniforms.maxSize,'value',0.01,0.06).step(0.001).name('陆地');gui.add(uniforms.minSize,'value',0.01,0.06).step(0.001).name('海洋');gui.addColor(params.colors,'base').name('基础色').onChange(val=>{earth&&earth.material.color.set(val);});复制代码

📌 如果想要了解更多关于 dat.GUI 的属性和方法,可以访问本文末尾提供的官方文档地址

💫 添加飞线和冲击波

这部分内容实现地球表层的飞线和冲击波效果 🌠,基本思路是:使用 THREE.Line 创建 10 条随机位置的飞线路径,通过 setPath 方法设置飞线的路径 然后通过 TWEEN 更新飞线和冲击波扩散动画,一条动画结束后,在终点的位置基础上重新调整飞线开始的位置,通过更新 Shader 参数 实现飞线和冲击波效果,并循环执行该过程,最后将飞线和冲击波关联到地球 🌐 上,具体实现如以下代码所示:

letmaxImpactAmount=10,impacts=[];lettrails=[];for(leti=0;i<maxImpactAmount;i++){impacts.push({impactPosition:newTHREE.Vector3().random().subScalar(0.5).setLength(5),impactMaxRadius:5*THREE.Math.randFloat(0.5,0.75),impactRatio:0,prevPosition:newTHREE.Vector3().random().subScalar(0.5).setLength(5),trailRatio:{value:0},trailLength:{value:0}});makeTrail(i);}//创建虚线材质和线网格并设置路径functionmakeTrail(idx){letpts=newArray(100*3).fill(0);letg=newTHREE.BufferGeometry();g.setAttribute('position',newTHREE.Float32BufferAttribute(pts,3));letm=newTHREE.LineDashedMaterial({color:params.colors.gradOuter,transparent:true,onBeforeCompile:shader=>{shader.uniforms.actionRatio=impacts[idx].trailRatio;shader.uniforms.lineLength=impacts[idx].trailLength;//片段着色器shader.fragmentShader=lineFragmentShader;}});//创建飞线letl=newTHREE.Line(g,m);l.userData.idx=idx;setPath(l,impacts[idx].prevPosition,impacts[idx].impactPosition,1);trails.push(l);}//飞线网格、起点位置、终点位置、顶点高度functionsetPath(l,startPoint,endPoint,peakHeight){letpos=l.geometry.attributes.position;letdivision=pos.count-1;letpeak=peakHeight||1;letradius=startPoint.length();letangle=startPoint.angleTo(endPoint);letarcLength=radius*angle;letdiameterMinor=arcLength/Math.PI;letradiusMinor=(diameterMinor*0.5)/cycle;letpeakRatio=peak/diameterMinor;letradiusMajor=startPoint.length()+radiusMinor;letbasisMajor=newTHREE.Vector3().copy(startPoint).setLength(radiusMajor);letbasisMinor=newTHREE.Vector3().copy(startPoint).negate().setLength(radiusMinor);lettri=newTHREE.Triangle(startPoint,endPoint,newTHREE.Vector3());letnrm=newTHREE.Vector3();tri.getNormal(nrm);letv3Major=newTHREE.Vector3();letv3Minor=newTHREE.Vector3();letv3Inter=newTHREE.Vector3();letvFinal=newTHREE.Vector3();for(leti=0;i<=division;i++){letdivisionRatio=i/division;letangleValue=angle*divisionRatio;v3Major.copy(basisMajor).applyAxisAngle(nrm,angleValue);v3Minor.copy(basisMinor).applyAxisAngle(nrm,angleValue+Math.PI*2*divisionRatio*1);v3Inter.addVectors(v3Major,v3Minor);letnewLength=((v3Inter.length()-radius)*peakRatio)+radius;vFinal.copy(v3Inter).setLength(newLength);pos.setXYZ(i,vFinal.x,vFinal.y,vFinal.z);}pos.needsUpdate=true;l.computeLineDistances();l.geometry.attributes.lineDistance.needsUpdate=true;impacts[l.userData.idx].trailLength.value=l.geometry.attributes.lineDistance.array[99];l.material.dashSize=3;}复制代码

添加动画过渡效果

for(leti=0;i<maxImpactAmount;i++){tweens.push({runTween:()=>{letpath=trails[i];letspeed=3;letlen=path.geometry.attributes.lineDistance.array[99];letdur=len/speed;lettweenTrail=newTWEEN.Tween({value:0}).to({value:1},dur*1000).onUpdate(val=>{impacts[i].trailRatio.value=val.value;});vartweenImpact=newTWEEN.Tween({value:0}).to({value:1},THREE.Math.randInt(2500,5000)).onUpdate(val=>{uniforms.impacts.value[i].impactRatio=val.value;}).onComplete(val=>{impacts[i].prevPosition.copy(impacts[i].impactPosition);impacts[i].impactPosition.random().subScalar(0.5).setLength(5);setPath(path,impacts[i].prevPosition,impacts[i].impactPosition,1);uniforms.impacts.value[i].impactMaxRadius=5*THREE.Math.randFloat(0.5,0.75);tweens[i].runTween();});tweenTrail.chain(tweenImpact);tweenTrail.start();}});}复制代码

📟 创建头部

头部机甲风格的形状是通过纯 CSS 实现的,利用 clip-path 属性,使用不同的裁剪方式创建元素的可显示区域,区域内的部分显示,区域外的隐藏。

.header  background #f9f002  clip-path polygon(0 0, 100% 0, 100% calc(100% - 35px), 75% calc(100% - 35px), 72.5% 100%, 27.5% 100%, 25% calc(100% - 35px), 0 calc(100% - 35px), 0 0)复制代码

📌 如果想了解关于 clip-path 的更多知识,可以访问文章末尾提供的 MDN 地址。

📊 添加两侧卡片

两侧的 卡片 🎴,也是机甲风格形状,同样由 clip-path 生成的。卡片有实心实心点状背景镂空背景三种基本样式。

.box  background-color #000  clip-path polygon(0px 25px, 26px 0px, calc(60% - 25px) 0px, 60% 25px, 100% 25px, 100% calc(100% - 10px), calc(100% - 15px) calc(100% - 10px), calc(80% - 10px) calc(100% - 10px), calc(80% - 15px) 100%, 80px calc(100% - 0px), 65px calc(100% - 15px), 0% calc(100% - 15px))  transition all .25s linear  &.inverse    border none    padding 40px 15px 30px    color #000    background-color var(--yellow-color)    border-right 2px solid var(--border-color)    &::before      content "T-71"      background-color #000      color var(--yellow-color)  &.dotted, &.dotted::after    background var(--yellow-color)    background-image radial-gradient(#00000021 1px, transparent 0)    background-size 5px 5px    background-position -13px -3px复制代码

卡片上的图表 📊,直接使用的是 Eachrts 插件,通过修改每个图表的配置来适配 赛博朋克 2077 的样式风格。

constchart_1=echarts.init(document.getElementsByClassName('chart_1')[0],'dark');chart_1&&chart_1.setOption(chart_1_option);复制代码

📌 Echarts 图标使用不是本文重点内容,想要了解更多细节内容,可访问其官网。

添加底部仪表盘

底部仪表盘主要用于数据展示,并且添加了 3雷达扫描动画,雷达 📡 形状则是通过 radial-gradient 径向渐变来实现的,然后利用 ::before::after 伪元素实现扫描动画效果,具体 keyframes 实现可以查看样式源码。

.radar  background: radial-gradient(center, rgba(32, 255, 77, 0.3) 0%, rgba(32, 255, 77, 0) 75%), repeating-radial-gradient(rgba(32, 255, 77, 0) 5.8%, rgba(32, 255, 77, 0) 18%, #20ff4d 18.6%, rgba(32, 255, 77, 0) 18.9%), linear-gradient(90deg, rgba(32, 255, 77, 0) 49.5%, #20ff4d 50%, #20ff4d 50%, rgba(32, 255, 77, 0) 50.2%), linear-gradient(0deg, rgba(32, 255, 77, 0) 49.5%, #20ff4d 50%, #20ff4d 50%, rgba(32, 255, 77, 0) 50.2%).radar:before  content ''  display block  position absolute  width 100%  height 100%  border-radius: 50%  animation blips  1.4s 5s infinite linear.radar:after  content ''  display block  background-image linear-gradient(44deg, rgba(0, 255, 51, 0) 50%, #00ff33 100%)  width 50%  height 50%  animation radar-beam 5s infinite linear  transform-origin: bottom right  border-radius 100% 0 0 0复制代码

🤳 添加交互

故障风格后期

点击第一个卡片上的按钮 START ,星际之旅进入 Hard 模式 😱,页面将会产生如下图所示的故障动画效果。它是通过引入 Three.js 内置的后期通道 GlitchPass 实现的,添加以下代码后,记得要在页面重绘动画中更新 composer

constcomposer=newEffectComposer(renderer);composer.addPass(newRenderPass(scene,camera));constglitchPass=newGlitchPass();composer.addPass(glitchPass);复制代码

地球点击事件

使用 Raycaster 给地球网格添加点击事件,在地球上 双击鼠标 🖱,会弹出一个提示框 💬,并会随机加载一些提示文案。

constraycaster=newTHREE.Raycaster();constmouse=newTHREE.Vector2();window.addEventListener('dblclick',event=>{mouse.x=(event.clientX/window.innerWidth)*2-1;mouse.y=-(event.clientY/window.innerHeight)*2+1;raycaster.setFromCamera(mouse,camera);constintersects=raycaster.intersectObjects(earth.children);if(intersects.length>0){this.setState({showModal:true,modelText:tips[Math.floor(Math.random()*tips.length)]});}},false);复制代码

🎥 添加入场动画等其他细节

最后,还添加了一些样式细节和动画效果,如头部和两侧卡片的入场动画、头部时间坐标文字闪烁动画、第一张卡片按钮故障风格动画Cyberpunk 2077 Logo阴影效果等。由于文章篇幅有限,不在这里细讲,感兴趣的朋友可以自己查看源码学习。也可以查看阅读我的另一篇文章 仅用CSS几步实现赛博朋克2077风格视觉效果 > 传送门 `🚪`[5] 查看更多细节内容。

总结

本文包含的新知识点主要包括:

  • THREE.Spherical 球体坐标系的应用

  • Shader 结合 TWEEN 实现飞线和冲击波动画效果

  • dat.GUI 调试工具库的使用

  • clip-path 创建不规则图形

  • Echarts 的基本使用方法

  • radial-gradient 创建雷达图形及动画

  • GlitchPass 添加故障风格后期

  • Raycaster 网格点击事件等

后续计划

本页面虽然已经做了很多效果和优化,但是还有很多改进的空间,后续我计划更新的内容包括:

  • 🌏 地球坐标和实际地理坐标结合,可以根据经纬度定位到国家、省份等具体位置

  • 💻 缩放适配不同屏幕尺寸

  • 📊 图表以及仪表盘展示一些真实的数据并且可以实时更新

  • 🌠 头部和卡片添加一些炫酷的描边动画

  • 🌟 添加宇宙星空粒子背景(有时间的话,现在的噪点背景也不错)

  • 🐌 性能优化

想了解其他前端知识或其他未在本文中详细描述的 Web 3D 开发技术相关知识,可阅读我往期的文章。转载请注明原文地址和作者。如果觉得文章对你有帮助,不要忘了一键三连哦 👍

关于本文

作者:dragonir

https://juejin.cn/post/7124116814937718797

往期推荐

中高级前端工程师都需要熟悉的技能--前端缓存-

低代码都做了什么?怎么实现 Low-Code?-

中杯超大杯中间的新选择—— Vue2.7 + Vite + TS 实践-

最后

  • 欢迎加我微信,拉你进技术群,长期交流学习...-

  • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...

点个在看支持我吧

查看原网页: mp.weixin.qq.com

⚠️ **GitHub.com Fallback** ⚠️