背景
近期工作有波及到数字大屏的需要,于是利用业余时间,联合 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 ⬜ 按钮会给页面增加故障格调前期 ⚡,双击地球会弹出随机提醒语弹窗。
实现
资源引入
引入开发必备的资源,其中除了根底的 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作者:dragonir链接: