背景
Facebook 近期将其母公司改名为 Meta,宣布正式开始进军 元宇宙 领域。本文主要讲述通过 Three.js + Blender 技术栈,实现 Meta 公司炫酷的 3D 动态 Logo,内容包括基础模型圆环、环面扭结、管道及模型生成、模型加载、添加动画、添加点击事件、更换材质等。
什么是元宇宙
元宇宙%20Metaverse%20一词源于%201992%20年尼尔·斯蒂芬森的%20《雪崩》,该书描述了一个平行于现实世界的虚拟世界%20Metaverse,所有现实生活中的人都有一个网络分身%20Avatar。维基百科%20对元宇宙的描述是:通过虚拟增强的物理现实,呈现收敛性和物理持久性特征的,基于未来互联网,具有链接感知和共享特征的%203D%20虚拟空间。
元宇宙的内涵是吸纳了信息革命%205G/6G、互联网革命%20web3.0、人工智能革命,以及%20VR、AR、MR,特别是游戏引擎在内的虚拟现实技术革命的成果,向人类展现出构建与传统物理世界平行的全息数字世界的可能性;引发了信息科学、量子科学,数学和生命科学的互动,改变科学范式;推动了传统的哲学、社会学甚至人文科学体系的突破;囊括了所有的数字技术。正如电影%20《头号玩家》%20的场景,在未来某一天,人们可以随时随地切换身份,自由穿梭于物理世界和数字世界,在虚拟空间和时间节点所构成的元宇宙中生活学习。
实现效果
进入正题,先来看看本文示例的实现效果。
%20在线预览:https://dragonir.github.io/3d-meta-logo%20(由于模型较大,加载进度可能比较缓慢,需要耐心等待)
开发实现
%20注意:上述示例动图展示的是试炼四,不想看试错过程(试炼一、试炼二、试炼三)的,可直接跳转到试炼四段落查看详细实现流程。失败流程中都列出了难点,知道解决方案的大佬请在评论区不吝赐教。
开发之前我们先观察一下%20Meta%20Logo,可以发现它是一个圆环经过对折扭曲形成的,因此实现它的时候可以从实现圆环开始。
试炼一:THREE.TorusGeometry
Three.js%20提供的基础几何体%20THREE.TorusGeometry(圆环),它是一种看起来像甜甜圈%20的简单图形。主要参数:
radius:可选。定义圆环的半径尺寸。默认值是%201。tube:可选。定义圆环的管子半径。默认值是%200.4。radialSegments:可选。定义圆环长度方向上的分段数。默认值是%208。tubularSegments:可选。定义圆环宽度方向上的分段数。默认值是%206。arc:可选。定义圆环绘制的长度。取值范围是%200%20到%202%20*%20π。默认值是%202%20*%20π(一个完整的圆)。语法示例:
THREE.TorusGeometry(radius,%20tube,%20radialSegments,%20tubularSegments,%20arc);
%20失败:没有找到扭曲圆环的方法。
试炼二:THREE.TorusKnotGeometry
THREE.TorusKnotGeometry%20可以用来创建三维环面扭结,环面扭结是一种比较特别的结,看上去像一根管子绕着它自己旋转了几圈。主要参数:
radius:可选。设置完整圆环的半径,默认值是%201。tube:可选。设置管道的半径,默认值是%200.4。radialSegments:可选。指定管道截面的分段数,段数越多,管道截面圆越光滑,默认值是%208。tubularSegments:可选。指定管道的分段数,段数越多,管道越光滑,默认值是%2064。p:可选。决定几何体将绕着其旋转对称轴旋转多少次,默认值是%202。q:可选。决定几何体将绕着其内部圆环旋转多少次,默认值是%203。语法示例:
THREE.TorusKnotGeometry(radius,%20tube,%20radialSegments,%20tubularSegments%20,%20p,%20q);
%20失败:没找到能够控制手动扭曲程度的方法。
试炼三:THREE.TubeGeometry
THREE.TubeGeometry%20沿着一条三维的样条曲线拉伸出一根管。你可以指定一些定点来定义路径,然后使用%20THREE.TubeGeometry%20创建这根管。主要参数:
path:该属性用一个%20THREE.SplineCurve3%20对象来指定管道应当遵循的路径。segments:该属性指定构建这个管所用的分段数。默认值为%2064.路径越长,指定的分段数应该越多。radius:该属性指定管的半径。默认值为%201.radiusSegments:该属性指定管道圆周的分段数。默认值为%208,分段数越多,管道看上去越圆。closed:如果该属性设置为%20true,管道的头和尾会连起来,默认值为%20false。代码示例
//%20...var%20controls%20=%20new%20function%20()%20{%20%20//%20点的位置坐标%20%20this.deafultpoints%20=%20[%20%20%20%20[0,%200.4,%20-0.4],%20%20%20%20[0.4,%200,%200],%20%20%20%20[0.4,%200.8,%200.4],%20%20%20%20[0,%200.4,%200.4],%20%20%20%20[-0.4,%200,%200],%20%20%20%20[-0.4,%200.8,%20-0.4],%20%20%20%20[0,%200.4,%20-0.4]%20%20]%20%20this.segments%20=%2064;%20%20this.radius%20=%201;%20%20this.radiusSegments%20=%208;%20%20this.closed%20=%20true;%20%20this.points%20=%20[];%20%20this.newPoints%20=%20function%20()%20{%20%20%20%20var%20points%20=%20[];%20%20%20%20for%20(var%20i%20=%200;%20i%20<%20controls.deafultpoints.length;%20i++)%20{%20%20%20%20%20%20var%20_x%20=%20controls.deafultpoints[i][0]%20*%2022;%20%20%20%20%20%20var%20_y%20=%20controls.deafultpoints[i][1]%20*%2022;%20%20%20%20%20%20var%20_z%20=%20controls.deafultpoints[i][2]%20*%2022;%20%20%20%20%20%20points.push(new%20THREE.Vector3(_x,%20_y,%20_z));%20%20%20%20}%20%20%20%20controls.points%20=%20points;%20%20%20%20controls.redraw();%20%20};%20%20this.redraw%20=%20function%20()%20{%20%20%20%20redrawGeometryAndUpdateUI(gui,%20scene,%20controls,%20function()%20{%20%20%20%20%20%20return%20generatePoints(controls.points,%20controls.segments,%20controls.radius,%20controls.radiusSegments,%20%20%20%20%20%20%20%20controls.closed);%20%20%20%20});%20%20};};controls.newPoints();function%20generatePoints(points,%20segments,%20radius,%20radiusSegments,%20closed)%20{%20%20if%20(spGroup)%20scene.remove(spGroup);%20%20spGroup%20=%20new%20THREE.Object3D();%20%20var%20material%20=%20new%20THREE.MeshBasicMaterial({%20color:%200xff0000,%20transparent:%20false%20});%20%20points.forEach(function%20(point)%20{%20%20%20%20var%20spGeom%20=%20new%20THREE.SphereGeometry(0.1);%20%20%20%20var%20spMesh%20=%20new%20THREE.Mesh(spGeom,%20material);%20%20%20%20spMesh.position.copy(point);%20%20%20%20spGroup.add(spMesh);%20%20});%20%20scene.add(spGroup);%20%20return%20new%20THREE.TubeGeometry(new%20THREE.CatmullRomCurve3(points),%20segments,%20radius,%20radiusSegments,%20closed);}//%20...
%20勉强成功:但是管道连成的圆环不够圆,实现完美的圆弧需要精确的坐标,暂时没找到坐标计算方法。
试炼四:Blender%20+%20Three.js
虽然使用%20THREE.TubeGeometry%20可以勉强实现,但是效果并不好,要实现圆滑的环,需要为管道添加精确的扭曲圆环曲线路径函数。由于数学能力有限%20️,暂时没找到扭曲圆弧路径计算的方法。因此决定从建模层面解决。
成功%20:但是手残的我使用%20Blender%20建模花费了大量的时间%20。
建模教程
逛%20B站%20的时候发现了这位大佬发的宝藏视频,刚好解决了自己的难题。
%20传送门:【动态设计教程】AE+blender能怎么玩?脸书元宇宙Meta动态logo已完全解析,100%学会
用Blender建模
使用%20Blender%20进行建模,并导出可携带动画的%20fbx%20格式,导出的时候不要忘记勾选%20烘焙动画%20选项。
加载依赖
<script%20src="./assets/libs/three.js"></script><script%20src="./assets/libs/loaders/FBXLoader.js"></script><script%20src="./assets/libs/inflate.min.js"></script><script%20src="./assets/libs/OrbitControls.js"></script><script%20src="./assets/libs/stats.js"></script>
场景初始化
var%20container,%20stats,%20controls,%20compose,%20camera,%20scene,%20renderer,%20light,%20clickableObjects%20=%20[],%20mixer,%20mixerArr%20=%20[],%20manMixer;var%20clock%20=%20new%20THREE.Clock();init();animate();function%20init()%20{%20%20container%20=%20document.createElement('div');%20%20document.body.appendChild(container);%20%20//%20场景%20%20scene%20=%20new%20THREE.Scene();%20%20scene.transparent%20=%20true;%20%20scene.fog%20=%20new%20THREE.Fog(0xa0a0a0,%20200,%201000);%20%20//%20透视相机:视场、长宽比、近面、远面%20%20camera%20=%20new%20THREE.PerspectiveCamera(60,%20window.innerWidth%20/%20window.innerHeight,%200.1,%201000);%20%20camera.position.set(0,%204,%2016);%20%20camera.lookAt(new%20THREE.Vector3(0,%200,%200));%20%20//%20半球光源:创建室外效果更加自然的光源%20%20light%20=%20new%20THREE.HemisphereLight(0xefefef);%20%20light.position.set(0,%2020,%200);%20%20scene.add(light);%20%20//%20平行光%20%20light%20=%20new%20THREE.DirectionalLight(0x2d2d2d);%20%20light.position.set(0,%2020,%2010);%20%20light.castShadow%20=%20true;%20%20scene.add(light);%20%20//%20环境光%20%20var%20ambientLight%20=%20new%20THREE.AmbientLight(0xffffff,%20.5);%20%20scene.add(ambientLight);%20%20//%20网格%20%20var%20grid%20=%20new%20THREE.GridHelper(100,%20100,%200xffffff,%200xffffff);%20%20grid.position.set(0,%20-10,%200);%20%20grid.material.opacity%20=%200.3;%20%20grid.material.transparent%20=%20true;%20%20scene.add(grid);%20%20renderer%20=%20new%20THREE.WebGLRenderer({%20antialias:%20true,%20alpha:%20true%20});%20%20renderer.setPixelRatio(window.devicePixelRatio);%20%20renderer.outputEncoding%20=%20THREE.sRGBEncoding;%20%20renderer.setSize(window.innerWidth,%20window.innerHeight);%20%20//%20背景色设置为透明%20%20renderer.setClearAlpha(0);%20%20//%20开启阴影%20%20renderer.shadowMap.enabled%20=%20true;%20%20container.appendChild(renderer.domElement);%20%20//%20添加镜头控制器%20%20controls%20=%20new%20THREE.OrbitControls(camera,%20renderer.domElement);%20%20controls.target.set(0,%200,%200);%20%20controls.update();%20%20window.addEventListener('resize',%20onWindowResize,%20false);%20%20//%20初始化性能插件%20%20stats%20=%20new%20Stats();%20%20container.appendChild(stats.dom);}//%20屏幕缩放function%20onWindowResize()%20{%20%20camera.aspect%20=%20window.innerWidth%20/%20window.innerHeight;%20%20camera.updateProjectionMatrix();%20%20renderer.setSize(window.innerWidth,%20window.innerHeight);}
%20想了解场景初始化的详细流程,可阅读我的另一篇文章《使用three.js实现炫酷的酸性风格3D页面》。
加载Logo模型
使用%20FBXLoader%20加载模型,并设置模型的位置和大小。
var%20loader%20=%20new%20THREE.FBXLoader();loader.load('assets/models/meta.fbx',%20function%20(mesh)%20{%20%20mesh.traverse(function%20(child)%20{%20%20%20%20if%20(child.isMesh)%20{%20%20%20%20%20%20child.castShadow%20=%20true;%20%20%20%20%20%20child.receiveShadow%20=%20true;%20%20%20%20}%20%20});%20%20mesh.rotation.y%20=%20Math.PI%20/%202;%20%20mesh.position.set(0,%201,%200);%20%20mesh.scale.set(0.05,%200.05,%200.05);%20%20scene.add(mesh);});
添加材质
本文%20Logo%20使用的是%20MeshPhysicalMaterial材质,它是一种%20PBR%20物理材质,可以更好的模拟光照计算,相比较高光网格材质%20MeshPhongMaterial%20渲染效果更逼真。使用%20THREE.TextureLoader%20为材质添加%20map%20属性来加载模型贴图。下图是金属质感的纹理贴图。
var%20texLoader%20=%20new%20THREE.TextureLoader();loader.load('assets/models/meta.fbx',%20function%20(mesh)%20{%20%20mesh.traverse(function%20(child)%20{%20%20%20%20if%20(child.isMesh)%20{%20%20%20%20%20%20if%20(child.name%20===%20'贝塞尔圆')%20{%20%20%20%20%20%20%20%20child.material%20=%20new%20THREE.MeshPhysicalMaterial({%20%20%20%20%20%20%20%20%20%20map:%20texLoader.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:捕获点击模型。如果你对源码感兴趣,请点赞+转发+关注+私信【logo】。
欢迎点赞+转发+关注!大家的支持是我分享最大的动力!!!
版权声明:本文转载于今日头条,版权归作者所有,如果侵权,请联系本站编辑删除