DEV Community

Cover image for Use Three.js to achieve a cool cyberpunk-style 3D digital earth screen ๐ŸŒ
dragonir
dragonir

Posted on

Use Three.js to achieve a cool cyberpunk-style 3D digital earth screen ๐ŸŒ

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 thchnologies๏ผŒThe main knowledge points involved in this article include๏ผšTHREE.Sphericalใ€ Shaderใ€Tweened flying line and shock wave animation effectsใ€ dat.GUIใ€ clip-pathใ€ Echartsใ€ radial-gradient create radar graphics and animations, 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 flying line animation and shock wave 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.

image_0

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 related๏ผŒ OrbitControls ใ€ TWEENใ€ mergeBufferGeometries merge modelsใ€ EffectComposer RenderPass GlitchPass effect animationใ€ lineFragmentShader is flying line's shader ใ€ Echarts๏ผŒFinally use echarts.use to make it work.

import './index.styl';
import React from 'react';
import * as dat from 'dat.gui';
// three.js related
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 related
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, /* ...*/ ]);
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ƒ 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 logo๏ผ› .aside ๏ผ› .footer dash board๏ผŒdisplay 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>
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”ฉ Scene initialization

Define some global variables and parameters, initialize the scene , camera , lens track controller , page zoom monitor , add page redraw update animation , etc.

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));
// Create a scene
const scene = new THREE.Scene();
// Create a camera
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, .01, 50);
camera.position.set(0, 0, 15.5);
// add camera controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = false;
// Page zoom listen and re-update scene and camera
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize( window.innerWidth, window.innerHeight );
}, false);
// Page redraw animation
renderer.setAnimationLoop( _ => {
  TWEEN.update();
  earth.rotation.y += 0.001;
  renderer.render(scene, camera);
});
Enter fullscreen mode Exit fullscreen mode

๐ŸŒ 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.

image_1

// Create spherical coordinates
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 },
  // land color block size
  maxSize: { value: .04 },
  // Ocean color block size
  minSize: { value: .025 },
  // shock wave height
  waveHeight: { value: .1 },
  // Shockwave range
  scaling: { value: 1 },
  // Shockwave Radial Gradient Inside Color
  gradInner: { value: new THREE.Color(params.colors.gradInner) },
  // Shockwave Radial Gradient Outside Color
  gradOuter: { value: new THREE.Color(params.colors.gradOuter) }
}
// Create a grid of 10000 flat dots and position them to spherical coordinates
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);
}
// Merge multiple meshes into one mesh
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;
    // Pass the image of the globe as a parameter to shader
    shader.uniforms.tex = { value: new THREE.TextureLoader().load(imgData) };
    shader.vertexShader = vertexShader;
    shader.fragmentShader = fragmentShader;
    );
  }
});
// Create a sphere
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);
Enter fullscreen mode Exit fullscreen mode

image_2

๐Ÿ”ง 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('land');
gui.add(uniforms.minSize, 'value', 0.01, 0.06).step(0.001).name('ocean');
gui.addColor(params.colors, 'base').name('base color').onChange(val => {
 earth && earth.material.color.set(val);
});
Enter fullscreen mode Exit fullscreen mode

image_3

๐Ÿ“Œ If you want to know more about the properties and methods of dat.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);
}
// Create a dashed material and line mesh and set the path
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;
      // fragment shader
      shader.fragmentShader = lineFragmentShader;
    }
  });
  // Create flying leads
  let l = new THREE.Line(g, m);
  l.userData.idx = idx;
  setPath(l, impacts[idx].prevPosition, impacts[idx].impactPosition, 1);
  trails.push(l);
}
// Flyline mesh, start position, end position, vertex height
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;
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

image_4

๐Ÿ“Ÿ 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)
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Œ If you want to know more about clip-path, you can visit the MDN address provided at the end of the article.

image_5

๐Ÿ“Š Add side cards

The two sides cards ๐ŸŽด 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
Enter fullscreen mode Exit fullscreen mode

The chart on the card ๐Ÿ“Š uses the Eachrts plugin directly, and adapts the style of Cyberpunk 2077 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);
Enter fullscreen mode Exit fullscreen mode

image_8

โฑ 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
Enter fullscreen mode Exit fullscreen mode

image_6

๐Ÿคณ Add interaction

glitch style post

Click the button on the first card START โฌœ , the interstellar journey enters Hard model ๐Ÿ˜ฑ 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);
Enter fullscreen mode Exit fullscreen mode

Earth click event

Use Raycaster to add click events to the earth grid, double-click the mouse on the earth ๐Ÿ–ฑ, a prompt box will pop up ๐Ÿ’ฌ, and some prompt text will be loaded randomly.

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);
Enter fullscreen mode Exit fullscreen mode

image_7

๐ŸŽฅ 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 with TWEEN 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.

Todo 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
  • ๐ŸŒ 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 follow me ๐Ÿ‘ .

Appendix

My 3D column can be accessed by clicking this link ๐Ÿ‘ˆ

refer to

Top comments (1)

Collapse
 
tris909 profile image
Tran Minh Tri

So damn sick, wow