Disclaimer: The graphic and model materials involved in this article are only for personal study, research and appreciation. Please do not re-modify, illegally spread, reprint, publish, commercialize, or conduct other profit-making activities.
background
There is a need for a large digital screen in my recent work, so I used my spare time to combine Three.js
with CSS to achieve cyberpunk 2077 style visual effects to achieve cool 3D
digital earth large screen page.页面React + Three.js + Echarts + stylus
技术栈,本文涉及到的主要知识点包括: THREE.Spherical
系的应用、 Shader
结合---232be773a5766656450258fab6515e8c TWEEN
飞线和冲击波动画效果、 dat.GUI
调试工具库的使用、 clip-path
创建不规则图形、 Echarts
的基本使用方法、 radial-gradient
-Create radar graphics and animations, radial-gradient
GlitchPass
Add glitch style later, Raycaster
grid click events, etc.
Effect
As shown in the figure below 👇
, the main header of the page, the cards on both sides, the bottom dashboard and the main body 3D
Earth 🌐
composed of 飞线
animation and 冲击波
animation 🌠
, through 🖱
the mouse can rotate and zoom the globe. Click the START
⬜
button on the first card to add a glitch style later stage ⚡
to the page, and double-click the globe to pop up a random prompt popup.
-
💻
This page is only suitablePC
, the large screen access effect is better. -
👁🗨
Online preview address 1: https://3d-eosin.vercel.app/#/earthDigital -
👁🗨
Online preview address 2: https://dragonir.github.io/3d/#/earthDigital
accomplish
📦
Resource import
Introduce the necessary resources for development, in addition to the basic React
and style sheets, dat.gui
are used to dynamically control page parameters, and the rest are mainly divided into two parts: Three.js相关, OrbitControls
、 TWEEN
补间动画控制、 mergeBufferGeometries
合并模型、 EffectComposer
RenderPass
GlitchPass
效果动画、 lineFragmentShader
是飞线的Shader
、 Echarts相关按需引入需要的组件,最后使用echarts.use
to make it work.
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, /* ...*/ ]);
📃
Page structure
The main structure of the page is shown in the following code, .webgl
for rendering 3D
digital earth; .header
is the top of the page, which includes time , date , interstellar coordinates , Cyberpunk 2077 Logo
、本人Github
仓库地址等; .aside
; .footer
仪表盘,展示一些Radar animation and text information; if you look closely, you can see that the background has a noise effect, .bg
is used to generate a noise background effect.
<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>
🔩
Scene initialization
Define some global variables and parameters, initialize the scene , camera , lens track controller , page zoom monitor , add page redraw update animation , etc. to initialize the scene.
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);
});
🌐
Create a dotted globe
The specific idea is to use THREE.Spherical
to create a spherical coordinate system 〽
, and then create 10000
plane grid points, convert their spatial coordinates into spherical coordinates, and use mergeBufferGeometries
to combine them into one grid. Then use a map image as shown below as the material, adjust the size and transparency of the dots according to the color distribution of the material image in shader
, and adjust the color and size ratio of the dots according to the parameters passed in . Then create a sphere SphereGeometry
, use the generated shader material, and add it to the scene. At this point, a point-shaped earth 🌐
model is completed, and the specific implementation is as follows.
// 创建球类坐标
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);
🔧
Add debugging tools
In order to adjust the style of the sphere in real time and the parameter adjustment of subsequent flylines and shock waves, the tool library dat.GUI
can be used. It can create a form to add to the page, and bind the page parameters by adjusting the parameters, sliders and values on the form. After the parameter value is changed, the screen can be updated in real time, so that there is no need to adjust the code in the editor and view it in the browser effect. The basic usage is as follows. In this example, you can click the keyboard ⌨
H key to display or hide the parameter form, and you can modify it through the form 🌐
Earth background color, flying line color, shock wave amplitude and other effects.
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);
});
📌
If you want to know more about the properties and methods ofdat.GUI
, you can visit the official document address provided at the end of this article
💫
Add flying leads and shock waves
This part of the content realizes the effect of flying lines and shock waves on the surface of the earth 🌠
. The basic idea is: use THREE.Line
create 10
setPath
flying line path at a random position, through- setPath
method to set the path of the flying line and then pass TWEEN
update the flying line and shock wave diffusion animation. Shader
parameters to achieve the effect of flying wire and shock wave, and execute the process cyclically, and finally associate the flying wire and shock wave to the earth 🌐
, the specific implementation is shown in the following code:
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;
}
Add animated transition effects
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();
}
});
}
📟 Create the head
The shape of the head mecha style is achieved by pure CSS
, using the clip-path
attribute, using different cropping methods to create the displayable area of the element, the part inside the area is displayed, the part outside the area is displayed 's hidden.
.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)
📌
If you want to know more aboutclip-path
, you can visit theMDN
address provided at the end of the article.
📊 Add side cards
The two sides 卡片
🎴
are also mecha-style shapes, also generated by clip-path
. Cards have three basic styles: solid , solid dotted background , and hollow background .
.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
The chart on the card 📊
, directly uses the Eachrts
plugin, and adapts the style of 赛博朋克 2077
fd62b445f1a9fa9cca0a84b68b91f385--- by modifying the configuration of each chart.
const chart_1 = echarts.init(document.getElementsByClassName('chart_1')[0], 'dark');
chart_1 && chart_1.setOption(chart_1_option);
📌
Echarts
The use of icons is not the focus of this article. For more details, you can visit its official website.
⏱
Add bottom dashboard
The bottom dashboard is mainly used for data display, and added 3
a radar scanning animation, radar 📡
shape is realized by radial-gradient
radial gradient, Then use the ::before
and ::after
pseudo-elements to achieve the scanning animation effect, specifically keyframes
to achieve the style source code.
.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
🤳
Add interaction
glitch style post
Click the button on the first card START
⬜
, the interstellar journey enters Hard 模式
😱
will generate the page as shown below glitch animation effect. It is achieved by introducing Three.js
built-in post-pass GlitchPass
, after adding the following code, remember to update composer
in the page redraw animation.
const composer = new EffectComposer(renderer);
composer.addPass( new RenderPass(scene, camera));
const glitchPass = new GlitchPass();
composer.addPass(glitchPass);
Earth click event
Raycaster
给地球网格添加点击事件,在地球上双击鼠标
🖱
,会弹出一个提示框---47684d16401ec545e205c57dd0d1c61d--- , 💬
会随机Load some prompt copy.
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);
🎥
Add entry animation and other details
Finally, some style details and animation effects have been added, such as the entry animation of the head and side cards, the head time coordinate text flashing animation , the first card button glitch style animation , the shadow of Cyberpunk 2077 Logo
effect , etc. Due to the limited space of the article, I will not elaborate here. Interested friends can view the source code and learn by themselves. Also check out my other article on Cyberpunk 2077-style visuals in just a few steps with CSS > Portal 🚪
for more details.
Summarize
The new knowledge points included in this article mainly include:
-
THREE.Spherical
Application of spherical coordinate system -
Shader
combine withTWEEN
to achieve flying line and shock wave animation effects -
dat.GUI
Use of debugging tool library -
clip-path
Create irregular shape -
Echarts
-
radial-gradient
Create radar graphics and animations -
GlitchPass
Added glitch style late -
Raycaster
Grid click events, etc.
Follow-up plans :
Although a lot of effects and optimizations have been made on this page, there is still a lot of room for improvement. The contents I plan to update in the future include:
-
🌏
The combination of earth coordinates and actual geographic coordinates can locate specific locations such as countries and provinces according to latitude and longitude -
💻
Scaling to fit different screen sizes -
📊
Charts and dashboards show some real data and can be updated in real time -
🌠
Add some cool stroke animations to the head and cards -
🌟
Add cosmic starry sky particle background (if you have time, the current noise background is also good) -
🐌
Performance optimization
If you want to learn about other front-end knowledge or other knowledge that is not described in detail in this article Web 3D
development technology related knowledge, you can read my previous articles. Please indicate the original address and author when reprinting . If you think the article is helpful to you, don't forget to click three links 👍 .
appendix
- My 3D column can be accessed by clicking this link 👈
- [1]. 🦊 Three.js implements a 3D open world game: Ali's multiverse
- [2]. 🔥 Three.js flame effect to realize the dynamic logo of Aerdun's Ring
- [3]. 🐼 Three.js Realize the 2022 Winter Olympics theme 3D fun page, including Bingdundun
-
...
- [1]. 📷 The front-end implements a very impressive browser-side scanning function
- [2]. 🌏 The Legend of Zelda: Breath of the Wild
- [3]. 😱 Realize cyberpunk 2077 style visual effects with CSS only a few steps
-
...
refer to
- [1]. https://threejs.org
- [2]. https://github.com/dataarts/dat.gui/blob/master/API.md
- [3]. https://echarts.apache.org/zh/index.html
- [4]. https://www.cnblogs.com/pangys/p/13276936.html
- [5]. https://developer.mozilla.org/zh-CN/docs/Web/CSS/gradient/radial-gradient
- [6]. https://developer.mozilla.org/zh-CN/docs/Web/CSS/clip-path
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。