使用Three.js实现炫酷的赛博朋克风格3D数字地球大屏
聲明:本文涉及圖文和模型素材僅用于個人學習、研究和欣賞,請勿二次修改、非法傳播、轉載、出版、商用、及進行其他獲利行為。
背景
近期工作有涉及到數字大屏的需求,于是利用業余時間,結合 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 是飛線的 Shader、Echarts相關按需引入需要的組件,最后使用 echarts.use 使其生效。
import?'./index.styl'; import?React?from?'react'; import?*?as?dat?from?'dat.gui'; //?three.js?相關 import?*?as?THREE?from?'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'; import?lineFragmentShader?from?'@/containers/EarthDigital/shaders/line/fragment.glsl'; //?echarts?相關 import?*?as?echarts?from?'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 就是用于生成噪點背景效果。
<div?className='earth_digital'><canvas?className='webgl'></canvas><header?className='hud?header'><header></header><aside?className='hud?aside?left'></aside><aside?className='hud?aside?right'></aside><footer?className='hud?footer'></footer><section?className="bg"></section> </div> 復制代碼🔩 場景初始化
定義一些全局變量和參數,初始化場景、相機、鏡頭軌道控制器、頁面縮放監聽、添加頁面重繪更新動畫等進行場景初始化。
const?renderer?=?new?THREE.WebGLRenderer({canvas:?document.querySelector('canvas.webgl'),antialias:?true,alpha:?true }); renderer.setSize(window.innerWidth,?window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio,?2)); //?創建場景 const?scene?=?new?THREE.Scene(); //?創建相機 const?camera?=?new?THREE.PerspectiveCamera(45,?window.innerWidth?/?window.innerHeight,?.01,?50); camera.position.set(0,?0,?15.5); //?添加鏡頭軌道控制器 const?controls?=?new?OrbitControls(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,使用生成的著色器材質,并將它添加到場景中。到此,一個點狀地球 🌐 模型就完成了,具體實現如下。
//?創建球類坐標 let?sph?=?new?THREE.Spherical(); let?dummyObj?=?new?THREE.Object3D(); let?p?=?new?THREE.Vector3(); let?geoms?=?[],?rad?=?5,?r?=?0; let?dlong?=?Math.PI?*?(3?-?Math.sqrt(5)); let?dz?=?2?/?counter; let?long?=?0; let?z?=?1?-?dz?/?2; let?params?=?{colors:?{?base:?'#f9f002',?gradInner:?'#8ae66e',?gradOuter:?'#03c03c'?},reset:?()?=>?{?controls.reset()?} } let?uniforms?=?{impacts:?{?value:?impacts?},//?陸地色塊大小maxSize:?{?value:?.04?},//?海洋色塊大小minSize:?{?value:?.025?},//?沖擊波高度waveHeight:?{?value:?.1?},//?沖擊波范圍scaling:?{?value:?1?},//?沖擊波徑向漸變內側顏色gradInner:?{?value:?new?THREE.Color(params.colors.gradInner)?},//?沖擊波徑向漸變外側顏色gradOuter:?{?value:?new?THREE.Color(params.colors.gradOuter)?} } //?創建10000個平面圓點網格并將其定位到球坐標 for?(let?i?=?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();let?g?=??new?THREE.PlaneGeometry(1,?1);g.applyMatrix4(dummyObj.matrix);g.translate(p.x,?p.y,?p.z);let?centers?=?[p.x,?p.y,?p.z,?p.x,?p.y,?p.z,?p.x,?p.y,?p.z,?p.x,?p.y,?p.z];let?uv?=?new?THREE.Vector2((sph.theta?+?Math.PI)?/?(Math.PI?*?2),?1.?-?sph.phi?/?Math.PI);let?uvs?=?[uv.x,?uv.y,?uv.x,?uv.y,?uv.x,?uv.y,?uv.x,?uv.y];g.setAttribute('center',?new?THREE.Float32BufferAttribute(centers,?3));g.setAttribute('baseUv',?new?THREE.Float32BufferAttribute(uvs,?2));geoms.push(g); } //?將多個網格合并為一個網格 let?g?=?mergeBufferGeometries(geoms); let?m?=?new?THREE.MeshBasicMaterial({color:?new?THREE.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:?new?THREE.TextureLoader().load(imgData)?};shader.vertexShader?=?vertexShader;shader.fragmentShader?=?fragmentShader;);} }); //?創建球體 const?earth?=?new?THREE.Mesh(g,?m); earth.rotation.y?=?Math.PI; earth.add(new?THREE.Mesh(new?THREE.SphereGeometry(4.9995,?72,?36),?new?THREE.MeshBasicMaterial({?color:?new?THREE.Color(0x000000)?}))); earth.position.set(0,?-.4,?0); scene.add(earth); 復制代碼🔧 添加調試工具
為了實時調整球體的樣式和后續飛線和沖擊波的參數調整,可以使用工具庫 dat.GUI。它可以創建一個表單添加到頁面,通過調整表單上面的參數、滑塊和數值等方式綁定頁面參數,參數值更改后可以實時更新畫面,這樣就不用一邊到編輯器調整代碼一邊到瀏覽器查看效果了。基本用法如下,本例中可以在頁面通過點擊鍵盤 ? H鍵顯示或隱藏參數表單,通過表單可以修改 🌐 地球背景色、飛線顏色、沖擊波幅度大小等效果。
const?gui?=?new?dat.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 參數 實現飛線和沖擊波效果,并循環執行該過程,最后將飛線和沖擊波關聯到地球 🌐 上,具體實現如以下代碼所示:
let?maxImpactAmount?=?10,?impacts?=?[]; let?trails?=?[]; for?(let?i?=?0;?i?<?maxImpactAmount;?i++)?{impacts.push({impactPosition:?new?THREE.Vector3().random().subScalar(0.5).setLength(5),impactMaxRadius:?5?*?THREE.Math.randFloat(0.5,?0.75),impactRatio:?0,prevPosition:?new?THREE.Vector3().random().subScalar(0.5).setLength(5),trailRatio:?{value:?0},trailLength:?{value:?0}});makeTrail(i); } //?創建虛線材質和線網格并設置路徑 function?makeTrail(idx){let?pts?=?new?Array(100?*?3).fill(0);let?g?=?new?THREE.BufferGeometry();g.setAttribute('position',?new?THREE.Float32BufferAttribute(pts,?3));let?m?=?new?THREE.LineDashedMaterial({color:?params.colors.gradOuter,transparent:?true,onBeforeCompile:?shader?=>?{shader.uniforms.actionRatio?=?impacts[idx].trailRatio;shader.uniforms.lineLength?=?impacts[idx].trailLength;//?片段著色器shader.fragmentShader?=?lineFragmentShader;}});//?創建飛線let?l?=?new?THREE.Line(g,?m);l.userData.idx?=?idx;setPath(l,?impacts[idx].prevPosition,?impacts[idx].impactPosition,?1);trails.push(l); } //?飛線網格、起點位置、終點位置、頂點高度 function?setPath(l,?startPoint,?endPoint,?peakHeight)?{let?pos?=?l.geometry.attributes.position;let?division?=?pos.count?-?1;let?peak?=?peakHeight?||?1;let?radius?=?startPoint.length();let?angle?=?startPoint.angleTo(endPoint);let?arcLength?=?radius?*?angle;let?diameterMinor?=?arcLength?/?Math.PI;let?radiusMinor?=?(diameterMinor?*?0.5)?/?cycle;let?peakRatio?=?peak?/?diameterMinor;let?radiusMajor?=?startPoint.length()?+?radiusMinor;let?basisMajor?=?new?THREE.Vector3().copy(startPoint).setLength(radiusMajor);let?basisMinor?=?new?THREE.Vector3().copy(startPoint).negate().setLength(radiusMinor);let?tri?=?new?THREE.Triangle(startPoint,?endPoint,?new?THREE.Vector3());let?nrm?=?new?THREE.Vector3();tri.getNormal(nrm);let?v3Major?=?new?THREE.Vector3();let?v3Minor?=?new?THREE.Vector3();let?v3Inter?=?new?THREE.Vector3();let?vFinal?=?new?THREE.Vector3();for?(let?i?=?0;?i?<=?division;?i++)?{let?divisionRatio?=?i?/?division;let?angleValue?=?angle?*?divisionRatio;v3Major.copy(basisMajor).applyAxisAngle(nrm,?angleValue);v3Minor.copy(basisMinor).applyAxisAngle(nrm,?angleValue?+?Math.PI?*?2?*?divisionRatio?*?1);v3Inter.addVectors(v3Major,?v3Minor);let?newLength?=?((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?(let?i?=?0;?i?<?maxImpactAmount;?i++)?{tweens.push({runTween:?()?=>?{let?path?=?trails[i];let?speed?=?3;let?len?=?path.geometry.attributes.lineDistance.array[99];let?dur?=?len?/?speed;let?tweenTrail?=?new?TWEEN.Tween({?value:?0?}).to({value:?1},?dur?*?1000).onUpdate(?val?=>?{impacts[i].trailRatio.value?=?val.value;});var?tweenImpact?=?new?TWEEN.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 屬性,使用不同的裁剪方式創建元素的可顯示區域,區域內的部分顯示,區域外的隱藏。
.headerbackground #f9f002clip-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 生成的??ㄆ?strong>實心、實心點狀背景、鏤空背景三種基本樣式。
.boxbackground-color #000clip-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&.inverseborder nonepadding 40px 15px 30pxcolor #000background-color var(--yellow-color)border-right 2px solid var(--border-color)&::beforecontent "T-71"background-color #000color var(--yellow-color)&.dotted, &.dotted::afterbackground var(--yellow-color)background-image radial-gradient(#00000021 1px, transparent 0)background-size 5px 5pxbackground-position -13px -3px 復制代碼卡片上的圖表 📊,直接使用的是 Eachrts 插件,通過修改每個圖表的配置來適配 賽博朋克 2077 的樣式風格。
const?chart_1?=?echarts.init(document.getElementsByClassName('chart_1')[0],?'dark'); chart_1?&&?chart_1.setOption(chart_1_option); 復制代碼📌 Echarts 圖標使用不是本文重點內容,想要了解更多細節內容,可訪問其官網。
? 添加底部儀表盤
底部儀表盤主要用于數據展示,并且添加了 3 個雷達掃描動畫,雷達 📡 形狀則是通過 radial-gradient 徑向漸變來實現的,然后利用 ::before 和 ::after 偽元素實現掃描動畫效果,具體 keyframes 實現可以查看樣式源碼。
.radarbackground: 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:beforecontent ''display blockposition absolutewidth 100%height 100%border-radius: 50%animation blips 1.4s 5s infinite linear .radar:aftercontent ''display blockbackground-image linear-gradient(44deg, rgba(0, 255, 51, 0) 50%, #00ff33 100%)width 50%height 50%animation radar-beam 5s infinite lineartransform-origin: bottom rightborder-radius 100% 0 0 0 復制代碼🤳 添加交互
故障風格后期
點擊第一個卡片上的按鈕 START ?,星際之旅進入 Hard 模式 😱,頁面將會產生如下圖所示的故障動畫效果。它是通過引入 Three.js 內置的后期通道 GlitchPass 實現的,添加以下代碼后,記得要在頁面重繪動畫中更新 composer。
const?composer?=?new?EffectComposer(renderer); composer.addPass(?new?RenderPass(scene,?camera)); const?glitchPass?=?new?GlitchPass(); composer.addPass(glitchPass); 復制代碼地球點擊事件
使用 Raycaster 給地球網格添加點擊事件,在地球上 雙擊鼠標 🖱,會彈出一個提示框 💬,并會隨機加載一些提示文案。
const?raycaster?=?new?THREE.Raycaster(); const?mouse?=?new?THREE.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);const?intersects?=?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
最后
歡迎關注【前端瓶子君】??ヽ(°▽°)ノ?
回復「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會很認真的解答喲!
回復「交流」,吹吹水、聊聊技術、吐吐槽!
回復「閱讀」,每日刷刷高質量好文!
如果這篇文章對你有幫助,「在看」是最大的支持
?》》面試官也在看的算法資料《《
“在看和轉發”就是最大的支持
總結
以上是生活随笔為你收集整理的使用Three.js实现炫酷的赛博朋克风格3D数字地球大屏的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 鱼眼校正c语言算法,一种鱼眼图像逆向经纬
- 下一篇: 重温马云英文演讲:最伟大的成功