Threejs實現(xiàn)物理運動模擬
一、物理運動原理
在 Three.js 中實現(xiàn)物理運動的核心思想是:
- Three.js 負(fù)責(zé)渲染:Three.js 是一個圖形庫,用于創(chuàng)建和渲染 3D 場景。
- 物理引擎負(fù)責(zé)計算:物理引擎(如 Cannon.js或 Ammo.js, 本文用的是前一種)模擬現(xiàn)實世界的物理規(guī)則,例如重力、碰撞、反彈等。
- 數(shù)據(jù)同步:在每一幀中,將物理引擎計算的結(jié)果(如物體的位置、速度、旋轉(zhuǎn)等)同步到 Three.js 的對象上。
二、實現(xiàn)步驟詳解
1. 初始化 Three.js 場景
在 initScene 方法中,創(chuàng)建 Three.js 的基本組件(場景、相機、渲染器等)。
function initScene() {
if (!canvas.value) return
makeRenderer(canvas.value.clientWidth, canvas.value.clientHeight)
makeScene()
makeCamera(canvas.value.clientWidth, canvas.value.clientHeight)
orbitControls()
addSky()
addGround()
}
- 初始化 WebGL 渲染器,并設(shè)置畫布大小和設(shè)備像素比。 makeRenderer
- 創(chuàng)建 Three.js 場景,并添加光源。 makeScene
- 創(chuàng)建透視相機,并設(shè)置初始位置。 makeCamera
- 添加軌道控制器,用于控制相機視角。 orbitControls
- 和 :分別添加天空和地面。 addSky``addGround
2. 創(chuàng)建物理世界
使用 Cannon.js 創(chuàng)建物理世界,并設(shè)置重力。
world = new Cannon.World() world.gravity.set(0, -9.82, 0) // 設(shè)置重力 (m/s2)
- world.gravity.set(0, -9.82, 0):設(shè)置重力方向為向下(Y 軸負(fù)方向),模擬地球重力。
3. 創(chuàng)建物體并同步到物理引擎
在 Three.js 和物理引擎中分別創(chuàng)建物體,并保持它們之間的同步。
創(chuàng)建球體(Three.js 對象)
sphere = new THREE.Mesh(
new THREE.SphereGeometry(2, 32, 32),
new THREE.MeshBasicMaterial({ color: 0xff11ff }),
)
sphere.position.set(0, 10, 0)
scene.add(sphere)
- 使用 THREE.SphereGeometry 創(chuàng)建一個半徑為 2 的球體。
- 將球體添加到 Three.js 場景中。
創(chuàng)建對應(yīng)的物理剛體(Cannon.js 對象)
const sphereBody = new Cannon.Body({
mass: 1,
shape: new Cannon.Sphere(2),
position: new Cannon.Vec3(sphere.position.x, sphere.position.y, sphere.position.z),
})
world.addBody(sphereBody)
- 使用 Cannon.Sphere 創(chuàng)建一個物理剛體,并設(shè)置質(zhì)量為 1。
- 將物理剛體添加到物理世界中。
添加地面
const groundBody = new Cannon.Body({
mass: 0,
shape: new Cannon.Plane(),
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(groundBody)
- 創(chuàng)建一個靜態(tài)的平面作為地面(mass: 0 表示靜態(tài)物體)。
- 使用 setFromEuler 旋轉(zhuǎn)平面使其水平。
4. 動畫循環(huán)
在每一幀中更新物理引擎的狀態(tài),并同步到 Three.js 場景。
function animate() {
animationFrameId = requestAnimationFrame(animate)
// 更新物理引擎
updatePhysic()
// 渲染場景
renderer.render(scene, camera)
}
function updatePhysic() {
world.step(1 / 60) // 模擬 60 幀每秒
sphere.position.copy(sphereBody.position) // 同步位置
}
- world.step(1 / 60):更新物理引擎的世界狀態(tài),模擬 60 幀每秒。
- sphere.position.copy(sphereBody.position):將物理引擎計算的結(jié)果同步到 Three.js 的球體對象上。
5. 處理窗口大小調(diào)整
當(dāng)窗口大小發(fā)生變化時,重新調(diào)整渲染器和相機的參數(shù)。
function onWindowResize() {
if (!canvas.value) return
const width = canvas.value.clientWidth
const height = canvas.value.clientHeight
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height)
}
- 調(diào)整相機的寬高比,并更新投影矩陣。
- 重新設(shè)置渲染器的尺寸。
三、完整流程總結(jié)
- 初始化 Three.js 場景:
- 創(chuàng)建場景、相機、渲染器。
- 添加光源、天空、地面等元素。
- 初始化物理引擎:
- 創(chuàng)建物理世界,并設(shè)置重力。
- 添加地面和物體的物理剛體。
- 動畫循環(huán):
- 在每一幀中更新物理引擎的狀態(tài)。
- 將物理引擎的結(jié)果同步到 Three.js 對象上。
- 渲染場景。
- 處理窗口大小調(diào)整:
- 監(jiān)聽窗口大小變化事件,動態(tài)調(diào)整相機和渲染器的參數(shù)。
四、關(guān)鍵點解析
1. 數(shù)據(jù)同步
- 物理引擎中的物體(如 sphereBody)和 Three.js 中的物體(如 sphere)需要保持同步。
- 在每一幀中,通過 sphere.position.copy(sphereBody.position) 實現(xiàn)位置同步。
2. 性能優(yōu)化
- 使用 requestAnimationFrame 創(chuàng)建動畫循環(huán),確保流暢的動畫效果。
- 設(shè)置物理引擎的更新頻率(如 world.step(1 / 60)),避免過度計算。
3. 物理材質(zhì)
- 在代碼中,定義了兩種物理材質(zhì)(groundMaterial 和 sphereMaterial),并通過 設(shè)置摩擦系數(shù)和彈性系數(shù)。 ContactMaterial
- 這些參數(shù)影響物體的碰撞行為,例如反彈高度和滑動距離。
五、源碼
<template>
<el-container style="width: 100%; height: 100%">
<!-- 頭部標(biāo)題 -->
<el-header class="header"> 物理運動</el-header>
<!-- 主要內(nèi)容區(qū)域,包含 Three.js 畫布 -->
<el-main class="canvas-container">
<canvas ref="canvas" class="canvas"></canvas>
</el-main>
</el-container>
</template>
<script lang="ts">
export default {
name: 'PhysicalMotion', // 使用多詞名稱
}
</script>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { Sky } from 'three/examples/jsm/objects/Sky'
import { ShaderMaterial, MathUtils } from 'three'
import * as Cannon from 'cannon'
// Three.js 相關(guān)變量聲明
// 場景
let scene: THREE.Scene
// 透視相機
let camera: THREE.PerspectiveCamera
// WebGL渲染器
let renderer: THREE.WebGLRenderer
// 天空
let sky: THREE.Mesh
// 動畫幀ID
let animationFrameId: number
let controls: InstanceType<typeof OrbitControls>
let world: InstanceType<typeof Cannon.World>
let sphere: THREE.Mesh
let sphereBody: InstanceType<typeof Cannon.Body>
// 畫布引用
const canvas = ref<HTMLCanvasElement>()
// 組件掛載時初始化場景并開始動畫
onMounted(() => {
initScene()
animate()
// 添加窗口大小改變事件監(jiān)聽
window.addEventListener('resize', onWindowResize)
})
// 初始化場景
function initScene() {
if (!canvas.value) return
makeRenderer(canvas.value.clientWidth, canvas.value.clientHeight)
makeScene()
// 創(chuàng)建圓形
sphere = new THREE.Mesh(
new THREE.SphereGeometry(2, 32, 32),
new THREE.MeshBasicMaterial({ color: 0xff11ff }),
)
sphere.position.set(0, 10, 0)
scene.add(sphere)
world = new Cannon.World()
world.gravity.set(0, -9.82, 0)
//創(chuàng)建物理材料
const groundMaterial = new Cannon.Material("groundMaterial")
const sphereMaterial = new Cannon.Material("sphereMaterial")
const contactMaterial = new Cannon.ContactMaterial(groundMaterial, sphereMaterial, {
friction: 0.3,
restitution: 0.5,
})
world.addContactMaterial(contactMaterial)
sphereBody = new Cannon.Body({
mass: 1,
shape: new Cannon.Sphere(2),
position: new Cannon.Vec3(sphere.position.x, sphere.position.y, sphere.position.z),
material: sphereMaterial,
})
world.addBody(sphereBody)
const groundBody = new Cannon.Body({
mass: 0,
shape: new Cannon.Plane(),
material: groundMaterial,
})
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(groundBody)
makeCamera(canvas.value.clientWidth, canvas.value.clientHeight)
orbitControls()
addSky()
addGround()
}
function updatePhysic(){
world.step(1 / 60)
sphere.position.copy(sphereBody.position)
}
function makeScene() {
// 創(chuàng)建場景
scene = new THREE.Scene()
//添加光源
const light = new THREE.DirectionalLight(0xffffff, 1)
scene.add(light)
}
function makeRenderer(width: number, height: number) {
// 創(chuàng)建WebGL渲染器
renderer = new THREE.WebGLRenderer({ canvas: canvas.value, antialias: true })
renderer.setSize(width, height)
// 設(shè)置設(shè)備像素比,確保在高分辨率屏幕上清晰顯示
renderer.setPixelRatio(window.devicePixelRatio)
}
function makeCamera(width: number, height: number) {
// 創(chuàng)建透視相機
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
// 設(shè)置相機位置
camera.position.set(0, 10, 20)
camera.lookAt(0, 0, 0)
}
function orbitControls() {
controls = new OrbitControls(camera, canvas.value)
// 限制攝像機的極角范圍(單位是弧度)
controls.minPolarAngle = 0 // 最小角度,0 表示水平視角
controls.maxPolarAngle = MathUtils.degToRad(85) // 最大角度,例如 85 度
// 可選:限制攝像機的縮放范圍
controls.minDistance = 5 // 最小距離
controls.maxDistance = 100 // 最大距離
}
function addSky() {
sky = new Sky()
sky.scale.setScalar(450000) // 設(shè)置天空的縮放比例
scene.add(sky)
const sun = new THREE.Vector3()
// 配置天空參數(shù)
const effectController = {
turbidity: 10,
rayleigh: 2,
mieCoefficient: 0.005,
mieDirectionalG: 0.8,
elevation: 2,
azimuth: 180,
exposure: renderer.toneMappingExposure,
}
const uniforms = (sky.material as ShaderMaterial).uniforms
uniforms['turbidity'].value = effectController.turbidity
uniforms['rayleigh'].value = effectController.rayleigh
uniforms['mieCoefficient'].value = effectController.mieCoefficient
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG
const phi = THREE.MathUtils.degToRad(90 - effectController.elevation)
const theta = THREE.MathUtils.degToRad(effectController.azimuth)
sun.setFromSphericalCoords(1, phi, theta)
uniforms['sunPosition'].value.copy(sun)
}
// 添加大地
function addGround() {
const groundGeometry = new THREE.PlaneGeometry(100, 100)
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x7ec850, // 綠色草地顏色
side: THREE.DoubleSide,
})
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
ground.rotation.x = Math.PI / 2 // 旋轉(zhuǎn)平面使其水平
scene.add(ground)
}
// 動畫循環(huán)函數(shù)
function animate() {
animationFrameId = requestAnimationFrame(animate)
controls.update()
updatePhysic()
// 渲染場景
renderer.render(scene, camera)
}
// 清理函數(shù)
function cleanup() {
// 取消動畫幀`
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
// 清理渲染器
if (renderer) {
// 移除所有事件監(jiān)聽器
renderer.domElement.removeEventListener('resize', onWindowResize)
// 釋放渲染器資源
renderer.dispose()
// 清空渲染器
renderer.forceContextLoss()
// 移除畫布
renderer.domElement.remove()
}
// 清理場景
if (scene) {
// 遍歷場景中的所有對象
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
// 釋放幾何體資源
object.geometry.dispose()
// 釋放材質(zhì)資源
if (Array.isArray(object.material)) {
object.material.forEach((material) => material.dispose())
} else {
object.material.dispose()
}
}
})
// 清空場景
scene.clear()
}
// 清理相機
if (camera) {
camera.clear()
}
}
// 窗口大小改變時的處理函數(shù)
function onWindowResize() {
if (!canvas.value) return
const width = canvas.value.clientWidth
const height = canvas.value.clientHeight
console.log('onWindowResize', width, height)
// 更新相機
camera.aspect = width / height
camera.updateProjectionMatrix()
// 更新渲染器
renderer.setSize(width, height)
// 確保渲染器的像素比與設(shè)備匹配
renderer.setPixelRatio(window.devicePixelRatio)
}
// 組件卸載時清理資源
onUnmounted(() => {
cleanup()
// 移除窗口大小改變事件監(jiān)聽
window.removeEventListener('resize', onWindowResize)
})
</script>
<style scoped>
/* 頭部樣式 */
.header {
text-align: center;
font-size: 20px;
font-weight: bold;
color: #333;
}
/* 畫布容器樣式 */
.canvas-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* 畫布樣式 // 保持畫布比例*/
.canvas {
width: 100%;
height: 100%;
}
</style>到此這篇關(guān)于Threejs實現(xiàn)物理運動模擬的文章就介紹到這了,更多相關(guān)Threejs 物理運動模擬內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript模擬的Ping效果代碼 (Web Ping)
JS雖然發(fā)送不了真正Ping的ICMP數(shù)據(jù)包,但Ping的本質(zhì)仍然是請求/回復(fù)的時間差,HTTP自然可以實現(xiàn)此功能.2011-03-03
深入淺析JavaScript中的in關(guān)鍵字和for-in循環(huán)
這篇文章主要介紹了JavaScript中的in關(guān)鍵字和for-in循環(huán),本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04
解決前后端交互數(shù)據(jù)出現(xiàn)精度丟失的多種方式
這篇文章主要為大家介紹了解決前后端交互數(shù)據(jù)出現(xiàn)精度丟失的多種方式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-04-04
Ajax實現(xiàn)省市區(qū)三級聯(lián)動實例代碼
這篇文章介紹了Ajax實現(xiàn)省市區(qū)三級聯(lián)動的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-04-04
不到200行 JavaScript 代碼實現(xiàn)富文本編輯器的方法
這篇文章主要介紹了不到200行 JavaScript 代碼實現(xiàn)富文本編輯器的方法,需要的朋友可以參考下2018-01-01
Bootstrap彈出框modal上層的輸入框不能獲得焦點問題的解決方法
這篇文章主要介紹了Bootstrap彈出框modal上層的輸入框不能獲得焦點問題的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12

