引言
個人網(wǎng)站是程序員的第二張簡歷。如果你有酷炫的個人網(wǎng)頁,面試官對你的好感度會蹭蹭蹭往上漲。
在疫情隔離期間,我用Three.js
和Ammo.js
制作了一個可交互的3D個人網(wǎng)頁。
在線預覽地址: www.ryan-floyd.com/
Three.js的3D世界
當我在Google Experiments閑逛時,我發(fā)現(xiàn)非常多的作品都是用three.js
寫的。
three.js
是一個讓3D網(wǎng)頁應用開發(fā)變得簡單的庫。它誕生于2010年,作者是Ricardo Cabello (Mr.doob),,在github上有超過1300多的貢獻者,在所有倉庫中star數(shù)排行第38。
當看到Google Experiments
上那些酷炫的3D效果后,我決定開始學習three.js
。
Three.js的工作機制
(3D應用的組件結(jié)構(gòu),圖片來自discoverthreejs.com)
Three.js
使得在瀏覽器展示3D圖像變得容易,它的底層是基于WebGL
,它使瀏覽器能借助系統(tǒng)顯卡在canvas中繪制3D畫面。
WebGL
自身只能繪制點(points)、線(lines)和三角形(triangles),而Three.js
對WebGL
進行了封裝,使我們能夠非常方便地創(chuàng)建 物體(objects), 紋理(textures), 進行 3D 計算等操作。
使用Three.js
,我們將所有物體(objects)添加到場景(scene)中,然后將需要渲染的數(shù)據(jù)傳遞給渲染器(renderer),渲染器負責將場景在 <canvas>
畫布上繪制出來。
(Three.js 應用架構(gòu),圖片來自threejsfundamentals.org)
對于一個 Three.js
應用,最核心的就是場景(scene object),上面是一張場景圖(scene graph)。
在一個3D引擎中,場景圖是一個層級結(jié)構(gòu)的樹狀圖,樹中的每一個節(jié)點代表空間中的一部分。這種結(jié)構(gòu)有點像DOM樹,但Three.js
的場景(scene)更像虛擬DOM,它只更新和渲染場景中有變化的部分。而這一切的基礎,是 Three.js 的 WebGLRenderer
類,它把我們的代碼轉(zhuǎn)換成 GPU 中的數(shù)據(jù),瀏覽器再將這些數(shù)據(jù)渲染出來。
場景中的物體,也叫Mesh
。在 Three.js
的世界中,Mesh 是由 幾何體Geometry
(決定物體形狀) + 材質(zhì)Material
(決定物體外觀)構(gòu)成。
場景中的另一個重要元素,就是相機camera
,它決定了場景中 哪些部分以怎樣的視覺效果 被繪制在canvas
畫布上。
然后是動畫,為了實現(xiàn)動畫,渲染器(renderer)通常使用requestAnimationFrame()
方法,以每秒60次的頻率將場景更新繪制在canvas
上。requestAnimationFrame()
方法的原理和使用可以參考MDN。
下面這個例子來自Three.js
官方文檔,創(chuàng)建了一個旋轉(zhuǎn)的 3D 立方體。
<html>
<head>
<title>My first three.js app</title>
<style>
body {
margin: 0;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script src="https://unpkg.com/three@0.119.0/build/three.js"></script>
<script>
//創(chuàng)建場景和相機
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
//創(chuàng)建渲染器,設置尺寸為窗口尺寸,并將渲染后的元素添加到body
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
//創(chuàng)建一個Mesh(綠色的3D立方體),并添加到場景中
var geometry = new THREE.BoxGeometry();
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
//設置照相機的位置
camera.position.z = 5;
//瀏覽器每次渲染的時候更新立方體的旋轉(zhuǎn)角度
var animate = function () {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
};
animate();
</script>
</body>
</html>
復制代碼
效果如下:
轉(zhuǎn)存失敗重新上傳取消
Ammo.js物理引擎
Ammo.js 是將 Bullet物理引擎 直接移植到JavaScript的產(chǎn)物(Bullet Physics是一個開源的物理模擬引擎)。我對物理引擎底層的工作原理理解得不太深入,簡而言之,物理引擎根據(jù)你傳入的參數(shù)(比如重力),創(chuàng)建循環(huán),在每次循環(huán)中更新狀態(tài),從而模擬出自然的物理運動和碰撞等效果。
循環(huán)中的物體(通常也是剛體),具有力、質(zhì)量、慣性、摩擦力等物理屬性。每次循環(huán),通過不斷檢查所有物體的位置、狀態(tài)和運動來檢測碰撞和交互。如果發(fā)生交互,對象位置將根據(jù)經(jīng)過的時間和對象的物理屬性進行更新。下面是我代碼中的一個片段,顯示了如何創(chuàng)建物理引擎循環(huán)以及如何將它添加到Three.js的sphere球體中。
//引入庫
import * as THREE from "three";
import * as Ammo from "./builds/ammo";
import {scene} from "./resources/world";
//初始化 Ammo.js 物理引擎
Ammo().then((Ammo) => {
// 創(chuàng)建物理世界
function createPhysicsWorld() {
//完全碰撞檢測算法
let collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
// 重疊對/碰撞的調(diào)度計算
let dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
// 所有可能碰撞對的寬相位碰撞檢測列表
let overlappingPairCache = new Ammo.btDbvtBroadphase();
// 使物體正確地交互,考慮重力、力、碰撞等
let constraintSolver = new Ammo.btSequentialImpulseConstraintSolver();
// 根據(jù)這些參數(shù)創(chuàng)建物理世界。 參考bullet physics文檔
let physicsWorld = new Ammo.btDiscreteDynamicsWorld(
dispatcher,
overlappingPairCache,
constraintSolver,
collisionConfiguration
);
// 添加重力
physicsWorld.setGravity(new Ammo.btVector3(0, -9.8, 0));
}
//創(chuàng)建球體
function createBall(){
//球體參數(shù)
let pos = {x: 0, y: 0, z: 0};
let radius = 2;
let quat = {x: 0, y: 0, z: 0, w: 1};
let mass = 3;
//three.js相關(guān)代碼
//創(chuàng)建球體并添加到場景中
let ball = new THREE.Mesh(new THREE.SphereBufferGeometry(radius), new THREE.MeshStandardMaterial({color: 0xffffff}));
ball.position.set(pos.x, pos.y, pos.z);
scene.add(ball);
//Ammo.js相關(guān)代碼
//設置位置和旋轉(zhuǎn)
let transform = new Ammo.btTransform();
transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
transform.setRotation(
new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w)
);
//設置物體運動
let motionState = new Ammo.btDefaultMotionState(transform);
//設置碰撞邊界框
let collisionShape = new Ammo.btSphereShape(radius);
collisionShape.setMargin(0.05);
//設置慣性
let localInertia = new Ammo.btVector3(0, 0, 0);
collisionShape.calculateLocalInertia(mass, localInertia);
//生成創(chuàng)建剛體(物體)的結(jié)構(gòu)信息
let rigidBodyStructure = new Ammo.btRigidBodyConstructionInfo(
mass,
motionState,
collisionShape,
localInertia
);
//基于上面的結(jié)構(gòu)信息創(chuàng)建物體
let body = new Ammo.btRigidBody(rigidBodyStructure);
//當物體運動時,為其添加摩擦力
body.setFriction(10);
body.setRollingFriction(10);
// 將物體添加到物理世界,這樣Ammo.js引擎才能不斷更新物體的狀態(tài)
physicsWorld.addRigidBody(body);
}
createPhysicsWorld();
createBall()
}
復制代碼
運動和交互
在Ammo.js模擬的物理世界中,交互是基于屬性和力計算的。
每個對象有一個邊界框(bounding box)屬性,物理引擎會根據(jù)這個邊界框來檢測物體的位置。
在每個動畫循環(huán)中檢查所有對象的邊界框后,如果任意兩個對象的邊界框位于同一位置,引擎將記錄為“碰撞”,并相應地更新對象。 對于剛體來說,這意味著阻止兩個物體處于同一位置。
下面是我的代碼片段,顯示了渲染循環(huán)和世界物理是如何更新的。
//渲染框架
function renderFrame() {
//記錄上一次渲染的時間
let deltaTime = clock.getDelta();
//基于用戶輸入,計算球會受到的力和產(chǎn)生的速度
moveBall();
//根據(jù)時間更新物理世界狀態(tài)
updatePhysics(deltaTime);
//進行渲染
renderer.render(scene, camera);
// 循環(huán)
requestAnimationFrame(renderFrame);
}
//更新物理世界狀態(tài)的方法定義
function updatePhysics(deltaTime) {
physicsWorld.stepSimulation(deltaTime, 10);
//遍歷“剛體”列表,并更新物理世界中的所有剛體狀態(tài)
for (let i = 0; i < rigidBodies.length; i++) {
//變量定義:three.js需要的meshObject,和ammo.js需要的ammoObject
let meshObject = rigidBodies[i];
let ammoObject = meshObject.userData.physicsBody;
//獲取物體當前運動狀態(tài)
let objectMotion = ammoObject.getMotionState();
//如果物體正在移動,則獲取物體的當前位置和旋轉(zhuǎn)信息
if (objectMotion) {
objectMotion.getWorldTransform(transform);
let mPosition = transform.getOrigin();
let mQuaternion = transform.getRotation();
// 更新物體的位置和旋轉(zhuǎn)狀態(tài)
meshObject.position.set(mPosition.x(), mPosition.y(), mPosition.z());
meshObject.quaternion.set(mQuaternion.x(), mQuaternion.y(), mQuaternion.z(), mQuaternion.w());
}
}
}
復制代碼
用戶輸入
我們希望用戶在桌面和觸摸屏移動設備上都能夠在應用中移動球體。
對于鍵盤事件,當按下箭頭鍵時,通過監(jiān)聽“keydown”和“keyup”事件對球體添加相應方向的力。
對于觸摸屏,在屏幕上創(chuàng)建了一個操縱桿控制器。然后,我們將“touchstart”、“touchmove”和“touchend”事件監(jiān)聽器添加到用于控制的div元素(控制器)中。
控制器會跟蹤用戶手指移動的起始、當前和結(jié)束坐標,然后在每次渲染時相應地更新球的受力。文章來源:http://www.zghlxwxcb.cn/news/detail-418512.html
下面只是控制器代碼的一個片段,展示了一些大致的概念。有關(guān)完整代碼,請從本文底部的源代碼地址獲取。文章來源地址http://www.zghlxwxcb.cn/news/detail-418512.html
// 在坐標平面上保持對當前球體運動的跟蹤
let moveDirection = { left: 0, right: 0, forward: 0, back: 0 };
//控制器div在屏幕上的位置坐標
let coordinates = { x: 0, y: 0 };
//保存觸摸事件的起始坐標的變量
let dragStart = null;
//創(chuàng)建控制器div元素
const stick = document.createElement("div");
//監(jiān)聽用戶觸摸點的移動
function handleMove(event) {
//沒有移動,返回
if (dragStart === null) return;
//有移動,獲取新的觸摸點的x、y坐標
if (event.changedTouches) {
event.clientX = event.changedTouches[0].clientX;
event.clientY = event.changedTouches[0].clientY;
}
//根據(jù)觸摸點的移動,計算出控制器div的實時坐標
const xDiff = event.clientX - dragStart.x;
const yDiff = event.clientY - dragStart.y;
const angle = Math.atan2(yDiff, xDiff);
const distance = Math.min(maxDiff, Math.hypot(xDiff, yDiff));
const xNew = distance * Math.cos(angle);
const yNew = distance * Math.sin(angle);
coordinates = { x: xNew, y: yNew };
//根據(jù)實時坐標更新樣式
stick.style.transform = `translate3d(${xNew}px, ${yNew}px, 0px)`;
//根據(jù)坐標計算出球的運動方向
touchEvent(coordinates);
}
//根據(jù)用戶的觸摸點移動坐標計算出球的運動方向
function touchEvent(coordinates) {
// 向右運動
if (coordinates.x > 30) {
moveDirection.right = 1;
moveDirection.left = 0;
// 向左運動
} else if (coordinates.x < -30) {
moveDirection.left = 1;
moveDirection.right = 0;
} else {
moveDirection.right = 0;
moveDirection.left = 0;
}
//向前運動
if (coordinates.y > 30) {
moveDirection.back = 1;
moveDirection.forward = 0;
//向后運動
} else if (coordinates.y < -30) {
moveDirection.forward = 1;
moveDirection.back = 0;
} else {
moveDirection.forward = 0;
moveDirection.back = 0;
}
}
到了這里,關(guān)于用Three.js打造酷炫3D個人網(wǎng)站(含源碼)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!