关于前端:使用Threejs实现炫酷的赛博朋克风格3D数字地球大屏-🌐

申明:本文波及图文和模型素材仅用于集体学习、钻研和观赏,请勿二次批改、非法流传、转载、出版、商用、及进行其余获利行为。

背景

近期工作有波及到数字大屏的需要,于是利用业余时间,联合 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:https://3d-eosin.vercel.app/#…
  • 👁‍🗨 在线预览地址2:https://dragonir.github.io/3d…

实现

📦 资源引入

引入开发必备的资源,其中除了根底的 React 和样式表之外,dat.gui 用于动态控制页面参数,其余残余的次要分为两局部:Three.js相干, OrbitControls 用于镜头轨道控制、TWEEN 用于补间动画管制、mergeBufferGeometries 用户合并模型、EffectComposer RenderPass GlitchPass 用于生成前期故障成果动画、 lineFragmentShader 是飞线的 ShaderEcharts相干按需引入须要的组件,最初应用 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 开发技术相干常识,可浏览我往期的文章。转载请注明原文地址和作者。如果感觉文章对你有帮忙,不要忘了一键三连哦 👍

附录

  • 我的3D专栏能够点击此链接拜访 👈
  • [1]. 🦊 Three.js 实现3D凋谢世界小游戏:阿狸的多元宇宙
  • [2]. 🔥 Three.js 火焰成果实现艾尔登法环动静logo
  • [3]. 🐼 Three.js 实现2022冬奥主题3D趣味页面,含冰墩墩
  • ...
  • [1]. 📷 前端实现很哇塞的浏览器端扫码性能
  • [2]. 🌏 前端瓦片地图加载之塞尔达传说旷野之息
  • [3]. 😱 仅用CSS几步实现赛博朋克2077格调视觉效果
  • ...

参考

  • [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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理