使用Three.js實現炫酷的賽博朋克風格3D數字地球大屏 🌐
- 2022 年 7 月 25 日
- 筆記
- CSS, javascript, three.js, 前端
聲明:本文涉及圖文和模型素材僅用於個人學習、研究和欣賞,請勿二次修改、非法傳播、轉載、出版、商用、及進行其他獲利行為。
背景
近期工作有涉及到數字大屏的需求,於是利用業餘時間,結合 Three.js
和 CSS實現賽博朋克2077風格視覺效果 實現炫酷 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/#/earthDigital👁🗨
在線預覽地址2://dragonir.github.io/3d/#/earthDigital
實現
📦
資源引入
引入開發必備的資源,其中除了基礎的 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;
// 將地球圖片作為參數傳遞給shader
shader.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
屬性,使用不同的裁剪方式創建元素的可顯示區域,區域內的部分顯示,區域外的隱藏。
.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
的樣式風格。
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
實現可以查看樣式源碼。
.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
。
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風格視覺效果 > 傳送門 🚪
查看更多細節內容。
總結
本文包含的新知識點主要包括:
THREE.Spherical
球體坐標系的應用Shader
結合TWEEN
實現飛線和衝擊波動畫效果dat.GUI
調試工具庫的使用clip-path
創建不規則圖形Echarts
的基本使用方法radial-gradient
創建雷達圖形及動畫GlitchPass
添加故障風格後期Raycaster
網格點擊事件等
後續計劃:
本頁面雖然已經做了很多效果和優化,但是還有很多改進的空間,後續我計劃更新的內容包括:
🌏
地球坐標和實際地理坐標結合,可以根據經緯度定位到國家、省份等具體位置💻
縮放適配不同屏幕尺寸📊
圖表以及儀錶盤展示一些真實的數據並且可以實時更新🌠
頭部和卡片添加一些炫酷的描邊動畫🌟
添加宇宙星空粒子背景(有時間的話,現在的噪點背景也不錯)🐌
性能優化
想了解其他前端知識或其他未在本文中詳細描述的
Web 3D
開發技術相關知識,可閱讀我往期的文章。轉載請註明原文地址和作者。如果覺得文章對你有幫助,不要忘了一鍵三連哦 👍。
附錄
-
...
-
[1]. 📷 前端實現很哇塞的瀏覽器端掃碼功能
-
[2]. 🌏 前端瓦片地圖加載之塞爾達傳說曠野之息
-
...
參考
- [1]. //threejs.org
- [2]. //github.com/dataarts/dat.gui/blob/master/API.md
- [3]. //echarts.apache.org/zh/index.html
- [4]. //www.cnblogs.com/pangys/p/13276936.html
- [5]. //developer.mozilla.org/zh-CN/docs/Web/CSS/gradient/radial-gradient
- [6]. //developer.mozilla.org/zh-CN/docs/Web/CSS/clip-path
作者:dragonir 本文地址://www.cnblogs.com/dragonir/p/16516254.html