乐趣区

关于前端:Threejs实现脸书元宇宙3D动态Logo

背景

Facebook 近期将其母公司改名为 Meta,发表正式开始进军 元宇宙 🪐畛域。本文次要讲述通过 Three.js + Blender 技术栈,实现 Meta 公司炫酷的 3D 动静 Logo,内容包含根底模型圆环、环面扭结、管道及模型生成、模型加载、增加动画、增加点击事件、更换材质等。

什么是元宇宙

元宇宙 Metaverse 一词源于 1992 年尼尔·斯蒂芬森的 《雪崩》,该书形容了一个平行于事实世界的虚拟世界 Metaverse,所有现实生活中的人都有一个网络分身 Avatar维基百科 对元宇宙的形容是: 通过虚构加强的物理事实,出现收敛性和物理持久性特色的,基于将来互联网,具备链接感知和共享特色的 3D 虚拟空间

元宇宙 的外延是吸纳了信息反动 5G/6G、互联网反动 web3.0、人工智能反动,以及 VRARMR,特地是游戏引擎在内的虚拟现实技术反动的成绩,向人类展现出构建与传统物理世界平行的全息数字世界的可能性;引发了信息科学、量子迷信,数学和生命科学的互动,扭转迷信范式;推动了传统的哲学、社会学甚至人文科学体系的冲破;囊括了所有的数字技术。正如电影 《头等玩家》 的场景,在将来某一天,人们能够随时随地切换身份,自在穿梭于物理世界和数字世界,在虚拟空间和工夫节点所形成的元宇宙中生存学习

实现成果

进入正题,先来看看本文示例的实现成果。

🔗 在线预览:https://dragonir.github.io/3d-meta-logo(因为模型较大,加载进度可能比拟迟缓,须要急躁期待)

开发实现

📌 留神:上述示例动图展现的是 试炼四 ,不想看试错过程(试炼一、试炼二、试炼三)的,可间接跳转到 试炼四 段落查看具体实现流程。失败流程中都列出了难点,晓得解决方案的大佬请在评论区不吝赐教。

开发之前咱们先察看一下 Meta Logo,能够发现它是一个 圆环通过对折扭曲造成的,因而实现它的时候能够从实现圆环开始。

试炼一:THREE.TorusGeometry

Three.js 提供的根底几何体 THREE.TorusGeometry(圆环),它是一种看起来像甜甜圈 🍩 的简略图形。主要参数:

  • radius:可选。定义圆环的半径尺寸。默认值是 1
  • tube:可选。定义圆环的管子半径。默认值是 0.4
  • radialSegments:可选。定义圆环长度方向上的分段数。默认值是 8
  • tubularSegments:可选。定义圆环宽度方向上的分段数。默认值是 6
  • arc:可选。定义圆环绘制的长度。取值范畴是 02 * π。默认值是 2 * π(一个残缺的圆)。

语法示例:

THREE.TorusGeometry(radius, tube, radialSegments, tubularSegments, arc);

😭 失败:没有找到扭曲圆环的办法。

试炼二:THREE.TorusKnotGeometry

THREE.TorusKnotGeometry 能够用来创立三维环面扭结,环面扭结是一种比拟特地的结,看上去像一根管子绕着它本人旋转了几圈。主要参数:

  • radius:可选。设置残缺圆环的半径,默认值是 1
  • tube:可选。设置管道的半径,默认值是 0.4
  • radialSegments:可选。指定管道截面的分段数,段数越多,管道截面圆越润滑,默认值是 8
  • tubularSegments:可选。指定管道的分段数,段数越多,管道越润滑,默认值是 64
  • p:可选。决定几何体将绕着其旋转对称轴旋转多少次,默认值是 2
  • q:可选。决定几何体将绕着其外部圆环旋转多少次,默认值是 3

语法示例:

THREE.TorusKnotGeometry(radius, tube, radialSegments, tubularSegments , p, q);

😭 失败:没找到可能管制手动扭曲水平的办法。

试炼三:THREE.TubeGeometry

THREE.TubeGeometry 沿着一条三维的样条曲线拉伸出一根管。你能够指定一些定点来定义门路,而后应用 THREE.TubeGeometry 创立这根管。主要参数:

  • path:该属性用一个 THREE.SplineCurve3 对象来指定管道该当遵循的门路。
  • segments:该属性指定构建这个管所用的分段数。默认值为 64. 门路越长,指定的分段数应该越多。
  • radius:该属性指定管的半径。默认值为 1.
  • radiusSegments:该属性指定管道圆周的分段数。默认值为 8,分段数越多,管道看上去越圆。
  • closed:如果该属性设置为 true,管道的头和尾会连起来,默认值为 false

代码示例

// ...
var controls = new function () {
  // 点的地位坐标
  this.deafultpoints = [[0, 0.4, -0.4],
    [0.4, 0, 0],
    [0.4, 0.8, 0.4],
    [0, 0.4, 0.4],
    [-0.4, 0, 0],
    [-0.4, 0.8, -0.4],
    [0, 0.4, -0.4]
  ]
  this.segments = 64;
  this.radius = 1;
  this.radiusSegments = 8;
  this.closed = true;
  this.points = [];
  this.newPoints = function () {var points = [];
    for (var i = 0; i < controls.deafultpoints.length; i++) {var _x = controls.deafultpoints[i][0] * 22;
      var _y = controls.deafultpoints[i][1] * 22;
      var _z = controls.deafultpoints[i][2] * 22;
      points.push(new THREE.Vector3(_x, _y, _z));
    }
    controls.points = points;
    controls.redraw();};
  this.redraw = function () {redrawGeometryAndUpdateUI(gui, scene, controls, function() {
      return generatePoints(controls.points, controls.segments, controls.radius, controls.radiusSegments,
        controls.closed);
    });
  };
};
controls.newPoints();
function generatePoints(points, segments, radius, radiusSegments, closed) {if (spGroup) scene.remove(spGroup);
  spGroup = new THREE.Object3D();
  var material = new THREE.MeshBasicMaterial({color: 0xff0000, transparent: false});
  points.forEach(function (point) {var spGeom = new THREE.SphereGeometry(0.1);
    var spMesh = new THREE.Mesh(spGeom, material);
    spMesh.position.copy(point);
    spGroup.add(spMesh);
  });
  scene.add(spGroup);
  return new THREE.TubeGeometry(new THREE.CatmullRomCurve3(points), segments, radius, radiusSegments, closed);
}
// ...

😊 勉强胜利:然而管道连成的圆环不够圆,实现完满的圆弧须要准确的坐标,临时没找到坐标计算方法。

试炼四:Blender + Three.js

尽管应用 THREE.TubeGeometry 能够勉强实现,然而成果并不好,要实现圆滑的环,须要为管道增加准确的扭曲圆环曲线门路函数。因为数学能力无限 🤕️,临时没找到扭曲圆弧门路计算的办法。因而决定从建模层面解决。

胜利 😄:然而手残的我应用 Blender 建模破费了大量的工夫 💔

建模教程

B 站 的时候发现了这位大佬发的宝藏视频,刚好解决了本人的难题。

🎦 传送门:【动静设计教程】AE+blender 能怎么玩?脸书元宇宙 Meta 动静 logo 已齐全解析,100% 学会

用 Blender 建模

应用 Blender 进行建模,并导出可携带动画的 fbx 格局,导出的时候不要遗记勾选 烘焙动画 选项。

加载依赖

<script src="./assets/libs/three.js"></script>
<script src="./assets/libs/loaders/FBXLoader.js"></script>
<script src="./assets/libs/inflate.min.js"></script>
<script src="./assets/libs/OrbitControls.js"></script>
<script src="./assets/libs/stats.js"></script>

场景初始化

var container, stats, controls, compose, camera, scene, renderer, light, clickableObjects = [], mixer, mixerArr = [], manMixer;
var clock = new THREE.Clock();
init();
animate();
function init() {container = document.createElement('div');
  document.body.appendChild(container);
  // 场景
  scene = new THREE.Scene();
  scene.transparent = true;
  scene.fog = new THREE.Fog(0xa0a0a0, 200, 1000);
  // 透视相机:视场、长宽比、近面、远面
  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(0, 4, 16);
  camera.lookAt(new THREE.Vector3(0, 0, 0));
  // 半球光源:创立室外成果更加天然的光源
  light = new THREE.HemisphereLight(0xefefef);
  light.position.set(0, 20, 0);
  scene.add(light);
  // 平行光
  light = new THREE.DirectionalLight(0x2d2d2d);
  light.position.set(0, 20, 10);
  light.castShadow = true;
  scene.add(light);
  // 环境光
  var ambientLight = new THREE.AmbientLight(0xffffff, .5);
  scene.add(ambientLight);
  // 网格
  var grid = new THREE.GridHelper(100, 100, 0xffffff, 0xffffff);
  grid.position.set(0, -10, 0);
  grid.material.opacity = 0.3;
  grid.material.transparent = true;
  scene.add(grid);
  renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.outputEncoding = THREE.sRGBEncoding;
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 背景色设置为通明
  renderer.setClearAlpha(0);
  // 开启暗影
  renderer.shadowMap.enabled = true;
  container.appendChild(renderer.domElement);
  // 增加镜头控制器
  controls = new THREE.OrbitControls(camera, renderer.domElement);
  controls.target.set(0, 0, 0);
  controls.update();
  window.addEventListener('resize', onWindowResize, false);
  // 初始化性能插件
  stats = new Stats();
  container.appendChild(stats.dom);
}
// 屏幕缩放
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

📌 想理解场景初始化的具体流程,可浏览我的另一篇文章《应用 three.js 实现炫酷的酸性格调 3D 页面》。

加载 Logo 模型

应用 FBXLoader 加载模型,并设置模型的地位和大小。

var loader = new THREE.FBXLoader();
loader.load('assets/models/meta.fbx', function (mesh) {mesh.traverse(function (child) {if (child.isMesh) {
      child.castShadow = true;
      child.receiveShadow = true;
    }
  });
  mesh.rotation.y = Math.PI / 2;
  mesh.position.set(0, 1, 0);
  mesh.scale.set(0.05, 0.05, 0.05);
  scene.add(mesh);
});

增加材质

本文 Logo 应用的是 MeshPhysicalMaterial材质,它是一种 PBR 物理材质,能够更好的模仿光照计算,相比拟高光网格材质 MeshPhongMaterial 渲染成果更真切。应用 THREE.TextureLoader 为材质增加 map 属性来加载模型贴图。下图是金属质感的纹理贴图。

var texLoader = new THREE.TextureLoader();
loader.load('assets/models/meta.fbx', function (mesh) {mesh.traverse(function (child) {if (child.isMesh) {if (child.name === '贝塞尔圆') {
        child.material = new THREE.MeshPhysicalMaterial({map: texLoader.load("./assets/images/metal.png"),
          metalness: .2,
          roughness: 0.1,
          exposure: 0.4
        });
      }
    }
  });
})

增加动画

  • AnimationMixer 对象是场景中特定对象的动画播放器。当场景中的多个对象独立动画时,能够为每个对象应用一个 AnimationMixer
  • AnimationMixer 对象的 clipAction 办法生成能够管制执行动画的实例。
loader.load('assets/models/meta.fbx', function (mesh) {
  mesh.animations.map(item => {
    mesh.traverse(child => {
      // 因为模型中有多个物体,并且各自有不同动画,示例中只为贝塞尔圆这个网格增加动画
      if (child.name === '贝塞尔圆') {let mixer = new THREE.AnimationMixer(child);
        mixerArr.push(mixer);
        let animationClip = item;
        animationClip.duration = 8;
        let clipAction = mixer.clipAction(animationClip).play();
        animationClip = clipAction.getClip();}
    })
  })
});

增加动画之后,不要忘了要在 requestAnimationFrame 中更新动画。

function animate() {renderer.render(scene, camera);
  // 取得前后两次执行该办法的工夫距离
  let time = clock.getDelta();
  // 更新 logo 动画
  mixerArr.map(mixer => {mixer && mixer.update(time);
  });
  // 更新人物动画
  manMixer && manMixer.update(time);
  stats.update();
  requestAnimationFrame(animate);
}

展现加载进度

FBXLoader 同时返回两个回调函数,能够像上面这样应用,用来展现模型加载过程展现以及加载失败的逻辑实现。

<div class="loading" id="loading">
  <p class="text"> 加载进度 <span id="progress">0%</span></p>
<div>
var loader = new THREE.FBXLoader();
loader.load('assets/models/meta.fbx', mesh => {}, res => {
  // 加载过程
  let progress = (res.loaded / res.total * 100).toFixed(0);
  document.getElementById('progress').innerText = progress;
  if (progress === 100) {document.getElementById('loading').style.display = 'none';
  }
}, err => {
  // 加载失败
  console.log(err)
});

实现成果

点击更换材质

监听页面的点击事件,通过 HREE.Raycaster 拿到以后点击对象,为了展现例子,我为点击对象更换了一种材质 THREE.MeshStandardMaterial,并赋予它随机的 color 色彩、metalness 金属质感以及 roughness 毛糙水平。

// 申明 raycaster 和 mouse 变量
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
function onMouseClick(event) {
  // 通过鼠标点击的地位计算出 raycaster 所须要的点的地位,以屏幕核心为原点,值的范畴为 - 1 到 1.
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
  // 通过鼠标点的地位和以后相机的矩阵计算出 raycaster
  raycaster.setFromCamera(mouse, camera);
  // 获取 raycaster 直线和所有模型相交的数组汇合
  let intersects = raycaster.intersectObjects(clickableObjects);
  if (intersects.length > 0) {console.log(intersects[0].object)
    let selectedObj = intersects[0].object;
    selectedObj.material = new THREE.MeshStandardMaterial({color: `#${Math.random().toString(16).slice(-6)}`,
      metalness: Math.random(),
      roughness: Math.random()})
  }
}
window.addEventListener('click', onMouseClick, false);

📌 更多对于网格材质的常识,可参考文章开端的链接。

加载人物模型

人物模型的加载流程和 Logo 模型加载流程是一样的。我增加了一个正在施展 龟派气功 的人物,没想到与 Logo 模型的旋转动画十分符合 😂

loader.load('assets/models/man.fbx', function (mesh) {mesh.traverse(function (child) {if (child.isMesh) {
      child.castShadow = true;
      child.receiveShadow = true;
    }
  });
  mesh.rotation.y = Math.PI / 2;
  mesh.position.set(-14, -8.4, -3);
  mesh.scale.set(0.085, 0.085, 0.085);
  scene.add(mesh);
  manMixer = new THREE.AnimationMixer(mesh);
  let animationClip = mesh.animations[0];
  let clipAction = manMixer.clipAction(animationClip).play();
  animationClip = clipAction.getClip();}, res => {let progress = (res.loaded / res.total * 100).toFixed(0);
  document.getElementById('progress').innerText = progress + '%';
  if (Number(progress) === 100) {document.getElementById('loading').style.display = 'none';
  }
}, err => {console.log(err)
});

本文示例人物模型来源于 mixamo.com,该网站有有上百种人物和上千种动作可自由组合,收费 下载。大家能够筛选本人喜爱的人物和动画动作来练习 Three.js

总结

本文中波及到的次要知识点包含:

  • THREE.TorusGeometry:圆环。
  • THREE.TorusKnotGeometry:环面扭结。
  • THREE.TubeGeometry:管道。
  • Blender: 建模。
  • FBXLoader: 加载模型,显示加载进度。
  • TextureLoader:加载材质。
  • THREE.AnimationMixer:加载动画。
  • THREE.Raycaster:捕捉点击模型。

🔗 残缺代码:https://github.com/dragonir/3d-meta-logo

参考资料

  • [1]. 应用 three.js 实现炫酷的酸性格调 3D 页面
  • [2]. ThreeJs 意识材质
  • [3]. Three 之 Animation 初印象
  • [4]. 什么是元宇宙?

作者:dragonir 本文地址:https://www.cnblogs.com/drago…

退出移动版