Threejs實(shí)現(xiàn)物理運(yùn)動(dòng)模擬
一、物理運(yùn)動(dòng)原理
在 Three.js 中實(shí)現(xiàn)物理運(yùn)動(dòng)的核心思想是:
- Three.js 負(fù)責(zé)渲染:Three.js 是一個(gè)圖形庫(kù),用于創(chuàng)建和渲染 3D 場(chǎng)景。
- 物理引擎負(fù)責(zé)計(jì)算:物理引擎(如 Cannon.js或 Ammo.js, 本文用的是前一種)模擬現(xiàn)實(shí)世界的物理規(guī)則,例如重力、碰撞、反彈等。
- 數(shù)據(jù)同步:在每一幀中,將物理引擎計(jì)算的結(jié)果(如物體的位置、速度、旋轉(zhuǎn)等)同步到 Three.js 的對(duì)象上。
二、實(shí)現(xiàn)步驟詳解
1. 初始化 Three.js 場(chǎng)景
在 initScene 方法中,創(chuàng)建 Three.js 的基本組件(場(chǎng)景、相機(jī)、渲染器等)。
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è)置畫(huà)布大小和設(shè)備像素比。 makeRenderer
- 創(chuàng)建 Three.js 場(chǎng)景,并添加光源。 makeScene
- 創(chuàng)建透視相機(jī),并設(shè)置初始位置。 makeCamera
- 添加軌道控制器,用于控制相機(jī)視角。 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àn)橄蛳拢╕ 軸負(fù)方向),模擬地球重力。
3. 創(chuàng)建物體并同步到物理引擎
在 Three.js 和物理引擎中分別創(chuàng)建物體,并保持它們之間的同步。
創(chuàng)建球體(Three.js 對(duì)象)
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)建一個(gè)半徑為 2 的球體。
- 將球體添加到 Three.js 場(chǎng)景中。
創(chuàng)建對(duì)應(yīng)的物理剛體(Cannon.js 對(duì)象)
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)建一個(gè)物理剛體,并設(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)建一個(gè)靜態(tài)的平面作為地面(mass: 0 表示靜態(tài)物體)。
- 使用 setFromEuler 旋轉(zhuǎn)平面使其水平。
4. 動(dòng)畫(huà)循環(huán)
在每一幀中更新物理引擎的狀態(tài),并同步到 Three.js 場(chǎng)景。
function animate() {
animationFrameId = requestAnimationFrame(animate)
// 更新物理引擎
updatePhysic()
// 渲染場(chǎng)景
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):將物理引擎計(jì)算的結(jié)果同步到 Three.js 的球體對(duì)象上。
5. 處理窗口大小調(diào)整
當(dāng)窗口大小發(fā)生變化時(shí),重新調(diào)整渲染器和相機(jī)的參數(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)整相機(jī)的寬高比,并更新投影矩陣。
- 重新設(shè)置渲染器的尺寸。
三、完整流程總結(jié)
- 初始化 Three.js 場(chǎng)景:
- 創(chuàng)建場(chǎng)景、相機(jī)、渲染器。
- 添加光源、天空、地面等元素。
- 初始化物理引擎:
- 創(chuàng)建物理世界,并設(shè)置重力。
- 添加地面和物體的物理剛體。
- 動(dòng)畫(huà)循環(huán):
- 在每一幀中更新物理引擎的狀態(tài)。
- 將物理引擎的結(jié)果同步到 Three.js 對(duì)象上。
- 渲染場(chǎng)景。
- 處理窗口大小調(diào)整:
- 監(jiān)聽(tīng)窗口大小變化事件,動(dòng)態(tài)調(diào)整相機(jī)和渲染器的參數(shù)。
四、關(guān)鍵點(diǎn)解析
1. 數(shù)據(jù)同步
- 物理引擎中的物體(如 sphereBody)和 Three.js 中的物體(如 sphere)需要保持同步。
- 在每一幀中,通過(guò) sphere.position.copy(sphereBody.position) 實(shí)現(xiàn)位置同步。
2. 性能優(yōu)化
- 使用 requestAnimationFrame 創(chuàng)建動(dòng)畫(huà)循環(huán),確保流暢的動(dòng)畫(huà)效果。
- 設(shè)置物理引擎的更新頻率(如 world.step(1 / 60)),避免過(guò)度計(jì)算。
3. 物理材質(zhì)
- 在代碼中,定義了兩種物理材質(zhì)(groundMaterial 和 sphereMaterial),并通過(guò) 設(shè)置摩擦系數(shù)和彈性系數(shù)。 ContactMaterial
- 這些參數(shù)影響物體的碰撞行為,例如反彈高度和滑動(dòng)距離。
五、源碼
<template>
<el-container style="width: 100%; height: 100%">
<!-- 頭部標(biāo)題 -->
<el-header class="header"> 物理運(yùn)動(dòng)</el-header>
<!-- 主要內(nèi)容區(qū)域,包含 Three.js 畫(huà)布 -->
<el-main class="canvas-container">
<canvas ref="canvas" class="canvas"></canvas>
</el-main>
</el-container>
</template>
<script lang="ts">
export default {
name: 'PhysicalMotion', // 使用多詞名稱(chēng)
}
</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)變量聲明
// 場(chǎng)景
let scene: THREE.Scene
// 透視相機(jī)
let camera: THREE.PerspectiveCamera
// WebGL渲染器
let renderer: THREE.WebGLRenderer
// 天空
let sky: THREE.Mesh
// 動(dòng)畫(huà)幀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>
// 畫(huà)布引用
const canvas = ref<HTMLCanvasElement>()
// 組件掛載時(shí)初始化場(chǎng)景并開(kāi)始動(dòng)畫(huà)
onMounted(() => {
initScene()
animate()
// 添加窗口大小改變事件監(jiān)聽(tīng)
window.addEventListener('resize', onWindowResize)
})
// 初始化場(chǎng)景
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)建場(chǎ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)建透視相機(jī)
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
// 設(shè)置相機(jī)位置
camera.position.set(0, 10, 20)
camera.lookAt(0, 0, 0)
}
function orbitControls() {
controls = new OrbitControls(camera, canvas.value)
// 限制攝像機(jī)的極角范圍(單位是弧度)
controls.minPolarAngle = 0 // 最小角度,0 表示水平視角
controls.maxPolarAngle = MathUtils.degToRad(85) // 最大角度,例如 85 度
// 可選:限制攝像機(jī)的縮放范圍
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)
}
// 動(dòng)畫(huà)循環(huán)函數(shù)
function animate() {
animationFrameId = requestAnimationFrame(animate)
controls.update()
updatePhysic()
// 渲染場(chǎng)景
renderer.render(scene, camera)
}
// 清理函數(shù)
function cleanup() {
// 取消動(dòng)畫(huà)幀`
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
// 清理渲染器
if (renderer) {
// 移除所有事件監(jiān)聽(tīng)器
renderer.domElement.removeEventListener('resize', onWindowResize)
// 釋放渲染器資源
renderer.dispose()
// 清空渲染器
renderer.forceContextLoss()
// 移除畫(huà)布
renderer.domElement.remove()
}
// 清理場(chǎng)景
if (scene) {
// 遍歷場(chǎng)景中的所有對(duì)象
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()
}
}
})
// 清空?qǐng)鼍?
scene.clear()
}
// 清理相機(jī)
if (camera) {
camera.clear()
}
}
// 窗口大小改變時(shí)的處理函數(shù)
function onWindowResize() {
if (!canvas.value) return
const width = canvas.value.clientWidth
const height = canvas.value.clientHeight
console.log('onWindowResize', width, height)
// 更新相機(jī)
camera.aspect = width / height
camera.updateProjectionMatrix()
// 更新渲染器
renderer.setSize(width, height)
// 確保渲染器的像素比與設(shè)備匹配
renderer.setPixelRatio(window.devicePixelRatio)
}
// 組件卸載時(shí)清理資源
onUnmounted(() => {
cleanup()
// 移除窗口大小改變事件監(jiān)聽(tīng)
window.removeEventListener('resize', onWindowResize)
})
</script>
<style scoped>
/* 頭部樣式 */
.header {
text-align: center;
font-size: 20px;
font-weight: bold;
color: #333;
}
/* 畫(huà)布容器樣式 */
.canvas-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* 畫(huà)布樣式 // 保持畫(huà)布比例*/
.canvas {
width: 100%;
height: 100%;
}
</style>到此這篇關(guān)于Threejs實(shí)現(xiàn)物理運(yùn)動(dòng)模擬的文章就介紹到這了,更多相關(guān)Threejs 物理運(yùn)動(dòng)模擬內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript模擬的Ping效果代碼 (Web Ping)
JS雖然發(fā)送不了真正Ping的ICMP數(shù)據(jù)包,但Ping的本質(zhì)仍然是請(qǐng)求/回復(fù)的時(shí)間差,HTTP自然可以實(shí)現(xiàn)此功能.2011-03-03
微信小程序之自定義組件的實(shí)現(xiàn)代碼(附源碼)
最近在項(xiàng)目開(kāi)發(fā)中,遇到好多雷同的頁(yè)面樣式,就想著可以將常用的功能模塊封裝成組件,方便在項(xiàng)目中使用和修改。這篇文章主要介紹了微信小程序之自定義組件的實(shí)現(xiàn)代碼(附源碼),需要的朋友可以參考下2018-08-08
Js制作簡(jiǎn)單彈出層DIV在頁(yè)面居中 中間顯示遮罩的具體方法
這篇文章介紹了Js制作簡(jiǎn)單彈出層DIV在頁(yè)面居中 中間顯示遮罩的具體方法,有需要的朋友可以參考一下2013-08-08
JS實(shí)現(xiàn)很酷的水波文字特效實(shí)例
這篇文章主要介紹了JS實(shí)現(xiàn)很酷的水波文字特效,實(shí)例分析了javascript操作圖層特效的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02
短視頻(douyin)去水印工具的實(shí)現(xiàn)代碼
這篇文章主要介紹了市面上短視頻(douyin)"去水印"的工具原來(lái)是這樣實(shí)現(xiàn)的,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
深入淺析JavaScript中的in關(guān)鍵字和for-in循環(huán)
這篇文章主要介紹了JavaScript中的in關(guān)鍵字和for-in循環(huán),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04
解決前后端交互數(shù)據(jù)出現(xiàn)精度丟失的多種方式
這篇文章主要為大家介紹了解決前后端交互數(shù)據(jù)出現(xiàn)精度丟失的多種方式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04
Ajax實(shí)現(xiàn)省市區(qū)三級(jí)聯(lián)動(dòng)實(shí)例代碼
這篇文章介紹了Ajax實(shí)現(xiàn)省市區(qū)三級(jí)聯(lián)動(dòng)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04
不到200行 JavaScript 代碼實(shí)現(xiàn)富文本編輯器的方法
這篇文章主要介紹了不到200行 JavaScript 代碼實(shí)現(xiàn)富文本編輯器的方法,需要的朋友可以參考下2018-01-01
Bootstrap彈出框modal上層的輸入框不能獲得焦點(diǎn)問(wèn)題的解決方法
這篇文章主要介紹了Bootstrap彈出框modal上層的輸入框不能獲得焦點(diǎn)問(wèn)題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12

