Facebook 近期將其母公司改名為 Meta,宣布正式開始進軍 元宇宙 🪐領域。本文主要講述通過 Three.js + Blender 技術棧,實現 Meta 公司炫酷的 3D 動態 Logo,內容包括基礎模型圓環、環面扭結、管道及模型生成、模型加載、添加動畫、添加點擊事件、更換材質等。
什麼是元宇宙「元宇宙 Metaverse」 一詞源於 1992 年尼爾·史蒂芬森的 《雪崩》,該書描述了一個平行於現實世界的虛擬世界 Metaverse,所有現實生活中的人都有一個網絡分身 Avatar。維基百科 對元宇宙的描述是:「通過虛擬增強的物理現實,呈現收斂性和物理持久性特徵的,基於未來網際網路,具有連結感知和共享特徵的 3D 虛擬空間」。
❝「元宇宙」的內涵是吸納了信息革命 5G/6G、網際網路革命 web3.0、人工智慧革命,以及 VR、AR、MR,特別是遊戲引擎在內的虛擬實境技術革命的成果,向人類展現出構建與傳統物理世界平行的全息數字世界的可能性;引發了信息科學、量子科學,數學和生命科學的互動,改變科學範式;推動了傳統的哲學、社會學甚至人文科學體系的突破;囊括了所有的數位技術。正如電影 《頭號玩家》 的場景,「在未來某一天,人們可以隨時隨地切換身份,自由穿梭於物理世界和數字世界,在虛擬空間和時間節點所構成的元宇宙中生活學習」。
❞實現效果進入正題,先來看看本文示例的實現效果。
🔗 在線預覽:https://dragonir.github.io/3d-meta-logo (由於模型較大,加載進度可能比較緩慢,需要耐心等待)
❞開發實現❝📌 注意:上述示例動圖展示的是「試煉四」,不想看試錯過程(試煉一、試煉二、試煉三)的,可直接跳轉到「試煉四」段落查看詳細實現流程。失敗流程中都列出了難點,知道解決方案的大佬請在評論區不吝賜教。
❞開發之前我們先觀察一下 Meta Logo,可以發現它是一個「圓環經過對摺扭曲形成的」,因此實現它的時候可以從實現圓環開始。
試煉一:THREE.TorusGeometryThree.js 提供的基礎幾何體 THREE.TorusGeometry(圓環),它是一種看起來像甜甜圈 🍩 的簡單圖形。主要參數:
radius:可選。定義圓環的半徑尺寸。默認值是 1。tube:可選。定義圓環的管子半徑。默認值是 0.4。radialSegments:可選。定義圓環長度方向上的分段數。默認值是 8。tubularSegments:可選。定義圓環寬度方向上的分段數。默認值是 6。arc:可選。定義圓環繪製的長度。取值範圍是 0 到 2 * π。默認值是 2 * π(一個完整的圓)。語法示例:
THREE.TorusGeometry(radius, tube, radialSegments, tubularSegments, arc);
❝😭 失敗:沒有找到扭曲圓環的方法。
❞試煉二:THREE.TorusKnotGeometryTHREE.TorusKnotGeometry 可以用來創建三維環面扭結,環面扭結是一種比較特別的結,看上去像一根管子繞著它自己旋轉了幾圈。主要參數:
radius:可選。設置完整圓環的半徑,默認值是 1。tube:可選。設置管道的半徑,默認值是 0.4。radialSegments:可選。指定管道截面的分段數,段數越多,管道截面圓越光滑,默認值是 8。tubularSegments:可選。指定管道的分段數,段數越多,管道越光滑,默認值是 64。p:可選。決定幾何體將繞著其旋轉對稱軸旋轉多少次,默認值是 2。q:可選。決定幾何體將繞著其內部圓環旋轉多少次,默認值是 3。語法示例:
THREE.TorusKnotGeometry(radius, tube, radialSegments, tubularSegments , p, q);😭 失敗:沒找到能夠控制手動扭曲程度的方法。
試煉三:THREE.TubeGeometryTHREE.TubeGeometry 沿著一條三維的樣條曲線拉伸出一根管。你可以指定一些定點來定義路徑,然後使用 THREE.TubeGeometry 創建這根管。主要參數:
path:該屬性用一個 THREE.SplineCurve3 對象來指定管道應當遵循的路徑。segments:該屬性指定構建這個管所用的分段數。默認值為 64.路徑越長,指定的分段數應該越多。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.TorusKnotGeometry:環面扭結。THREE.AnimationMixer:加載動畫。❝🔗 完整代碼:https://github.com/dragonir/3d-meta-logo
❞參考資料[1]. 使用three.js實現炫酷的酸性風格3D頁面