当前位置:   article > 正文

3D数据可视化大屏,超炫酷 -_大屏3d路劲图

大屏3d路劲图

图片

最近上线的项目如上。

本次将会围绕这一大屏应用的部分技术进行分享,其中包含以下内容:

  • 路径

  • 能量光罩于噪声处理

  • bloom辉光

  • 飞线

  • 视频材质的应用

1. 路径

路径可以说是我们在可视化开发中较为常用了。

线的显示本身是路径,飞线的方向需要路径,物体按照某轨道移动也需要路径。

1.1 路径移动

路径移动的实现很简单,使用THREE.CurvePath插值工具配合动画工具就可以达到效果。

这里与@tweenjs/tween.js配合使用,编写了一个简单的函数,让某一物体沿着特定路径均匀移动。

  1. /**
  2.  * @param {THREE.CurvePath} curve 曲线工具
  3.  * @param {Objectobject 要移动的Object3d对象
  4.  * @param {Number} duration 动画时间
  5.  */
  6. export function pathNavigation(curve, object, duration = 2000) {
  7.   return new Promise((resolve) => {
  8.     const tween = new TWEEN.Tween({ t: 0 });
  9.     tween.to({ t: 1 }, duration);
  10.     tween.onUpdate(({ t }) => {
  11.       object.position.copy(curve.getPoint(t)); // 每帧更新位置
  12.     });
  13.     tween.onComplete(resolve);
  14.     tween.start();
  15.   });
  16. }
  17. // 使用方法:
  18. const curve = new THREE.CurvePath();
  19. curve.push(/* line1 */);
  20. curve.push(/* line2 */);
  21. curve.push(/* line3 */);
  22. const geometry = new THREE.BoxGeometry( 111 );
  23. const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
  24. const cube = new THREE.Mesh( geometry, material );
  25. scene.add( cube );
  26. pathNavigation(curve, cube, 5000)

1.2 路径工具

通常我们的路径都是直角拐线,或者就是一个贝塞尔曲线实现的抛物线。

这里我们将二者结合写了一个简单的工具,在两个线段的拐角处会用三维二次贝塞尔曲线实现圆角拐角

该组件继承自THREE.CurvePath,以支持相应的函数。

分别使用THREE.LineCurve3作为直线、THREE.QuadraticBezierCurve3作为拐角。

图片

路径工具

  1. import * as THREE from 'three';
  2. class CustomCurve extends THREE.CurvePath {
  3.   constructor(nodes = [], radius = 0.8) {
  4.     super();
  5.     radius = 0.5 + radius / 2;
  6.     if (nodes.length < 2) {
  7.       return;
  8.     }
  9.     nodes.forEach((item, index=> {
  10.       if (index) { // filter: first
  11.         const end = new THREE.Vector3(...item);
  12.         const start = new THREE.Vector3(...nodes[index - 1]);
  13.         let left = start.clone();
  14.         let right = end.clone();
  15.         if (index !== 1) {
  16.           left = start.clone().sub(end).multiplyScalar(radius).add(end);
  17.         }
  18.         if (nodes.length !== index + 1) {
  19.           right = end.clone().sub(start).multiplyScalar(radius).add(start);
  20.         }
  21.         this.curves.push(new THREE.LineCurve3(leftright));
  22.       }
  23.       if (index && nodes.length !== index + 1) {  // filter: first and last
  24.         const center = new THREE.Vector3(...item);
  25.         const start = new THREE.Vector3(...nodes[index - 1]);
  26.         const end = new THREE.Vector3(...nodes[index + 1]);
  27.         this.curves.push(
  28.           new THREE.QuadraticBezierCurve3(
  29.             center.clone().sub(start).multiplyScalar(radius).add(start),
  30.             center,
  31.             end.clone().sub(center).multiplyScalar(1 - radius).add(center),
  32.           ),
  33.         );
  34.       }
  35.     });
  36.   }
  37. }
  38. export default CustomCurve;

图片

路径移动

将物体换为THREE.Sprite以实现字体、图标的移动。


2. 能量光罩

图片

能量光罩

能量光罩,本质是一个半球,并对他的纹理进行加工。

整个模块包含四个部分:

  • SphereGeometry: 半球

  • ShaderMaterial: shader材质

  • texture: 一张贴图,用于实现扫描效果

  • glsl - 边缘发光

  • glsl - 噪声处理

JS代码:

  1. // 首先实现一个半球
  2. const geometry = new THREE.SphereGeometry(
  3.   5,
  4.   36,
  5.   36,
  6.   0,
  7.   Math.PI * 2,
  8.   0,
  9.   (Math.PI / 180* 90,
  10. );
  11. // 为他增加一个shader材质:
  12. const material = new THREE.ShaderMaterial({
  13.   uniforms: {
  14.     c: { type'f'value1.5 }, // 系数
  15.     p: { type'f'value4 },   // 强度
  16.     backgroundTexture: {          // 用于实现扫描效果的贴图
  17.       type't'
  18.       value: texture 
  19.     },
  20.     offset,                       // 扫描的偏移量
  21.     u_resolution: {               // 用于生成噪声
  22.       value: new THREE.Vector2(500500
  23.     },
  24.     u_timetime,                 // 噪声随时间变化
  25.     glowColor,                    // 光罩的颜色
  26.     viewVector: {                 // 相机位置
  27.       type'v3'value: camera.position 
  28.     },
  29.   },
  30.   vertexShader: vertex,
  31.   fragmentShader: fragment,
  32.   side: THREE.FrontSide,
  33.   depthWrite: false,
  34.   transparent: true,
  35. });

texture

图片

贴图

这里使用一张黑色的alpha渐变贴图。

图片

渐变

通过这张贴图来映射整个光罩扫描部分的透明度。

顶点着色器

顶点着色器主要为光罩的边缘发光提供计算。

  1. uniform vec3 viewVector;
  2. varying vec2 vUv;
  3. uniform float c;
  4. uniform float p;
  5. varying float intensity;
  6. void main()
  7. {
  8. vUv = uv;
  9. vec3 vNormal = normalize(normalMatrix * normal);
  10. vec3 vNormel = normalize(normalMatrix * viewVector);
  11. intensity = pow(c - dot(vNormal, vNormel), p); // 供片源着色器使用
  12. gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  13. }

片元着色器

片元着色器运行主要的纹理计算代码。其中包含了噪声处理、扫描与内发光的混合计算。

在这里推荐一本在线交互式书籍《The Book of Shaders》,噪声处理的部分来自于其中第十一章Noise 噪声

噪声处理代码引自:Noise 噪声[1]

  1. #ifdef GL_ES
  2. precision mediump float;
  3. #endif
  4. uniform vec2 u_resolution;
  5. uniform float u_time;
  6. uniform float offset;
  7. uniform vec3 glowColor;
  8. uniform sampler2D backgroundTexture;
  9. varying float intensity;
  10. varying vec2 vUv;
  11. vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
  12. vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
  13. vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
  14. float snoise(vec2 v) {
  15. const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0
  16. 0.366025403784439, // 0.5*(sqrt(3.0)-1.0)
  17. -0.577350269189626, // -1.0 + 2.0 * C.x
  18. 0.024390243902439); // 1.0 / 41.0
  19. vec2 i = floor(v + dot(v, C.yy) );
  20. vec2 x0 = v - i + dot(i, C.xx);
  21. vec2 i1;
  22. i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
  23. vec4 x12 = x0.xyxy + C.xxzz;
  24. x12.xy -= i1;
  25. i = mod289(i); // Avoid truncation effects in permutation
  26. vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
  27. + i.x + vec3(0.0, i1.x, 1.0 ));
  28. vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
  29. m = m*m ;
  30. m = m*m ;
  31. vec3 x = 2.0 * fract(p * C.www) - 1.0;
  32. vec3 h = abs(x) - 0.5;
  33. vec3 ox = floor(x + 0.5);
  34. vec3 a0 = x - ox;
  35. m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
  36. vec3 g;
  37. g.x = a0.x * x0.x + h.x * x0.y;
  38. g.yz = a0.yz * x12.xz + h.yz * x12.yw;
  39. return 2500.0 * dot(m, g); // 这里改变了噪声处理的参数
  40. }
  41. void main() {
  42. // 使用 vUv 替代 gl_FragCoord, 否则会以摄像机的角度绘制平面纹理
  43. // vec2 st = gl_FragCoord.xy/u_resolution.xy;
  44. vec2 st = vUv * 1.0;
  45. st.x *= u_resolution.x / u_resolution.y;
  46. vec3 color = vec3(0.0);
  47. vec2 pos = vec2(st*3.);
  48. float DF = 0.0;
  49. // Add a random position
  50. float a = 0.0;
  51. vec2 vel = vec2(u_time*.1);
  52. DF += snoise(pos+vel)*.25+.25;
  53. // Add a random position
  54. a = snoise(pos*vec2(cos(u_time*0.15),sin(u_time*0.1))*0.1)*3.1415;
  55. vel = vec2(cos(a),sin(a));
  56. DF += snoise(pos+vel)*.25+.25;
  57. color = vec3( smoothstep(.7,.75,fract(DF)) );
  58. // offset随着时间在0 - 1之间不断变化
  59. // 带入到获取alpha贴图的参数中做到贴图不断从上到下扫过
  60. vec4 background = texture2D(backgroundTexture, vec2(vUv.x, vUv.y + offset));
  61. background.a = clamp(background.a, 0.3, 0.9); // 因为最后与结果相乘,0.3控制整个光照的最低亮度,0.9控制最高亮度,如果开启辉光需要适当降低最低亮度
  62. float opacity = max(intensity, color.x) * background.a;
  63. gl_FragColor = vec4(glowColor, opacity);
  64. }

至此实现了能量光罩,同学们可以对不满意的地方自行定制修改增加参数。

其他

当前的实现因为要使用边缘发光的效果,导致无法开启双面贴图。

图片

这里可以选择将边缘发光去掉。顶点着色器只保留vUv与gl_position的计算即可,片元着色器如下:

  1. // float opacity = max(intensity, color.x) * background.a;
  2. float opacity = max(0.5, color.x)* background.a;

图片

这样看起来更像一个光罩。

3. 辉光

辉光会赋予场景灵魂。

光 是人类看见事物的媒介,同时光也会刺激人类的视觉感官。

与普通的灯光不同。threejs会以后期处理的形式处理辉光效果。

本次开发中未涉及到真实场景的模拟所以选择了Bloom辉光。

UnrealBloomPass 辉光通道

这里将官网的例子套了过来,简单实现了一下

  1. const BLOOM_SCENE = 5// 辉光所在层数
  2. const renderScene = new RenderPass(scene, camera);
  3. const bloomPass = new UnrealBloomPass(
  4. new THREE.Vector2(window.innerWidth, window.innerHeight),
  5.   1.5,
  6.   0.4,
  7.   0.85,
  8. );
  9. bloomPass.threshold = bloomOptions.threshold;
  10. bloomPass.strength = bloomOptions.strength;
  11. bloomPass.radius = bloomOptions.radius;
  12. const bloom = new EffectComposer(renderer);
  13. bloom.renderToScreen = false;
  14. bloom.addPass(renderScene);
  15. // 眩光通道bloomPass插入到composer
  16. bloom.addPass(bloomPass);
  17. bloomLayer = new THREE.Layers();
  18. bloomLayer.set(BLOOM_SCENE);
  19. const vertexShader = `
  20.   varying vec2 vUv;
  21.   void main() {
  22.     vUv = uv;
  23.     gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
  24.   }`;
  25. const fragmentShader = `
  26.   uniform sampler2D baseTexture;
  27.   uniform sampler2D bloomTexture;
  28.   varying vec2 vUv;
  29.     void main() {
  30.     gl_FragColor = ( texture2D( baseTexture, vUv ) + vec41.0 ) * texture2D( bloomTexture, vUv ) );
  31.   }`;
  32. const finalPass = new ShaderPass(
  33.   new THREE.ShaderMaterial({
  34.     uniforms: {
  35.       baseTexture: { valuenull },
  36.       bloomTexture: { value: bloom.renderTarget2.texture },
  37.     },
  38.     vertexShader,
  39.     fragmentShader,
  40.     defines: {},
  41.   }),
  42.   'baseTexture',
  43. );
  44. finalPass.needsSwap = true;
  45. const finalComposer = new EffectComposer(renderer);
  46. finalComposer.addPass(renderScene);
  47. finalComposer.addPass(finalPass);

将原renderer.render(scene, camera)替换为:

  1. scene.traverse(darkenNonBloomed); // 隐藏不需要辉光的物体
  2. bloom.render();
  3. scene.traverse(restoreMaterial); // 还原
  4. finalComposer.render(); 

其中darkenNonBloomedrestoreMaterial两个函数针对BLOOM_SCENE层进行过滤

  1. const materials = {};
  2. const bloomIgnore = [];
  3. const darkMaterial = new THREE.MeshBasicMaterial({ color: 'black' });
  4. function darkenNonBloomed(obj) {
  5.   if (obj instanceof THREE.Scene) { // 此处忽略Scene,否则场景背景会被影响
  6.     materials.scene = obj.background;
  7.     obj.background = null;
  8.     return;
  9.   }
  10.   if (
  11.     obj instanceof THREE.Sprite || // 此处忽略Sprite
  12.     bloomIgnore.includes(obj.type) ||
  13.     (obj.isMesh && bloomLayer.test(obj.layers) === false// 判断与辉光是否同层
  14.   ) {
  15.     materials[obj.uuid] = obj.material;
  16.     obj.material = darkMaterial;
  17.   }
  18. }
  19. function restoreMaterial(obj) {
  20.   if (obj instanceof THREE.Scene) {
  21.     obj.background = materials.scene;
  22.     delete materials.scene;
  23.     return;
  24.   }
  25.   if (materials[obj.uuid]) {
  26.     obj.material = materials[obj.uuid];
  27.     delete materials[obj.uuid];
  28.   }
  29. }

当我们在使用时,将需要辉光的物体加入BLOOM_SCENE层即可。

  1. const geometry = new THREE.BoxGeometry(100100100);
  2. const material = new THREE.MeshPhongMaterial({ color: 0x0033ff });
  3. const cube = new THREE.Mesh(geometry, material);
  4. cube.layers.enable(BLOOM_SCENE);
  5. scene.addMesh(cube);
  6. animation = () => {
  7.     cube.rotation.x += Math.PI / 180 / 5;
  8.     cube.rotation.y += Math.PI / 180 / 5;
  9.     cube.rotation.z += Math.PI / 180 / 5;
  10. };

效果:

图片

盒子

4. 飞线

在之前的文章中分享过使用顶点着色器实现的飞线。但这种飞线有着较为明显的缺陷。

图片

飞线异常

在本次分享中会分享两种弥补这一缺陷的实现方法

  • MeshLine

  • TubeGeometry

4.1 MeshLine

MeshLine[2]是一个扩展库,能让我们绘制出实心有宽度的线条。

使用

  1. import * as THREE from 'three';
  2. import { MeshLine, MeshLineMaterial } from './meshline.js';
  3. const geometry = new THREE.Geometry();
  4. geometry.vertices = [
  5.   // ... THREE.Vector3,
  6. ];
  7. // 代码生成材质
  8. const getTexture = (length, lineColor, lightColor, isHalf) => {
  9.   const canvas = document.createElement('canvas');
  10.   canvas.width = 256;
  11.   canvas.height = 1;
  12.   const ctx = canvas.getContext('2d');
  13.   const gradient = ctx.createLinearGradient(002561);
  14.   gradient.addColorStop(0, lineColor);
  15.   gradient.addColorStop(isHalf ? length : length / 2, lightColor);
  16.   gradient.addColorStop(length, lineColor);
  17.   gradient.addColorStop(length, lineColor);
  18.   gradient.addColorStop(1, lineColor);
  19.   ctx.fillStyle = gradient;
  20.   ctx.fillRect(002561);
  21.   const texture = new THREE.Texture(canvas);
  22.   texture.needsUpdate = true;
  23.   texture.wrapS = THREE.RepeatWrapping;
  24.   texture.wrapT = THREE.RepeatWrapping;
  25.   return texture;
  26. };
  27. const meshLine = new MeshLine();
  28. meshLine.setGeometry(geometry);
  29. const texture = getTexture(length, lineColor, lightColor, isHalf);
  30. texture.anisotropy = 16;
  31. texture.wrapS = THREE.RepeatWrapping;
  32. texture.wrapT = THREE.RepeatWrapping;
  33. const material = new MeshLineMaterial({
  34.   map: texture,             // 材质
  35.   useMap: true,             // 使用材质
  36.   lineWidth: 2,             // 线宽
  37.   sizeAttenuation: false,   // 是否随距离衰减
  38.   transparent: true,        // 开启透明度
  39. });
  40. const { width, height } = getCanvasSize();
  41. material.uniforms.resolution.value.set(width, height);
  42. const mesh = new THREE.Mesh(meshLine.geometry, material);
  43. const tween = new TWEEN.Tween(material.uniforms.offset.value// 飞线移动动画
  44.   .to({ x: material.uniforms.offset.value.x - 1 }, duration)
  45.   .delay(delay)
  46.   .repeat(repeat)
  47.   .start();

参数:

  1. const defaultOptions = {
  2.   speed: 0.3,
  3.   lineWidth: 2,
  4.   length0.3,
  5.   isHalf: false,
  6.   lineColor: 'rgba(171,157,245,0.2)',
  7.   lightColor: 'rgba(239,238,255,1)',
  8.   duration: 1000,
  9.   delay: 0,
  10.   repeat: Infinity,
  11. };

图片

飞线

这一方式的缺陷是无法随着摄像机与线之间的距离变化大小。

4.2 TubeGeometry

管道几何体可以很好的解决Meshline的缺陷。

图片

管道

这个实现方法原本就是用来实现管道的,但在开发时刚好发现他可以用来实现有宽度、距离感的飞线。

先看一下效果:

图片

管道飞线

实现方法只是使用了THREE.TubeGeometry不需要写shader

  1. const texture = new THREE.TextureLoader().load(
  2.   // 贴图引用本篇第二章能量罩扫光的白色版本
  3. );
  4. texture.wrapS = THREE.RepeatWrapping;
  5. texture.wrapT = THREE.RepeatWrapping;
  6. texture.repeat.x = 1;
  7. texture.repeat.y = 1;
  8. texture.rotation = Math.PI / 90// 旋转贴图,或者做一张旋转好的贴图。
  1. import * as THREE from 'three';
  2. import PathFactory from './Path'// 引用本篇第一章的路径工具
  3. const speed = 0.01// 飞线移动速度
  4. const path = [
  5.   [-110, -500],
  6.   [505050],
  7.   [10, -5010],
  8.   [50100100],
  9.   [50100111],
  10. ];
  11. const pathInstence = new PathFactory(path, cornerRadius);
  12. const stripGeo = new THREE.TubeBufferGeometry(  // 定义管道
  13.   pathInstence,
  14.   Math.round(pathInstence.getLength() / 2),
  15.   0.5,
  16.   8,
  17.   false,
  18. );
  19. const stripMat = new THREE.MeshBasicMaterial({
  20.   color,             // 定义颜色,会与白色贴图混合
  21.   map: texture,      // 贴图
  22.   transparent: true// 开启透明度
  23.   depthWrite: false// 管道飞线的关键
  24.   side: THREE.DoubleSide,
  25. });
  26. const tube = new THREE.Mesh(stripGeo, stripMat);
  27. this.object3d.add(tube);
  28. this.tube = tube;
  29. function animation() { // render tick
  30.   texture.offset.y += speed;
  31. }

当然,用shader一定可以实现性能更高、效果更好的飞线。

如果想要控制管道飞线的长度,可以采用手动生成贴图的方式。

5. 视频材质的应用

也许你会感叹这么炫的效果是怎么实现的做这么炫真的这么快做得完吗。也许看到这里你已经知道怎么实现了。但我还是要说一句

不是只有用着色器才能实现特效,还可以用素材来代替

看到这里,你可能已经猜到哪里被视频替代了。

……

没错,就是它!

图片

揭秘

没猜到的同学看见这一幕相信一定比看见特效更加震惊。(O_o)??

炫酷的特效其实只是一段视频素材贴在scene.background上。

使用它很简单

  1. <video id="video" loop autoplay muted style="display: none;">
  2.   <source src="你的视频资源.mp4" type="video/mp4">
  3. </video>
  1. const videoDom = document.getElementById('video');
  2. scene.background = new THREE.VideoTexture(video);

但无法用在移动端。

这一手段的应用范围很广:

  • 我们可以给地面贴一个光效扩散的视频来做扫描效果。

  • 某个闪烁的标签使用视频来代替。

  • 将星光闪烁的视频贴给某个模型来实现换肤。

  • 其他种种

当然,这一方法会极大增加静态文件体积。

不过方法提出来就是给人用的,我们要做到不独独依赖某项技术。

多一个实现方法总比只有一个实现方法要好。你一定会做到使用shader来重新替代视频。加油!

参考文档

  • Noise 噪声: https://thebookofshaders.com/11/?lan=ch

  • MeshLine: https://github.com/spite/THREE.MeshLine

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/96154
推荐阅读
相关标签
  

闽ICP备14008679号