Appearance
球碰撞地面反弹---使用 oimo.js
fps: 0
点击运行
使用Oimo
<template>
<div>
<div class="flex space-between">
<div>fps: {{ fps }}</div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
</div>
<div>使用Oimo</div>
<canvas v-if="isRunning" id="collisionBall" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
Engine,
Scene,
ArcRotateCamera,
Vector3,
Color3,
Color4,
HemisphericLight,
MeshBuilder,
StandardMaterial,
Texture,
OimoJSPlugin,
PhysicsImpostor
} from 'babylonjs'
import {
AdvancedDynamicTexture,
StackPanel,
Control,
TextBlock,
} from 'babylonjs-gui'
let sceneResources, adt
const max = 10
const y = 20
const fps = ref(0)
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
sceneResources = await initScene()
} else {
isRunning.value = false
destroy()
}
}
const initScene = async () => {
const ele = document.getElementById("collisionBall") as any
ele.addEventListener('wheel', function(event) {
// 根据需要处理滚动
// 例如,可以修改相机的半径或角度
event.preventDefault() // 阻止默认滚动行为
})
const engine: any = new Engine(ele, true, {
preserveDrawingBuffer: true,
stencil: true,
disableWebGL2Support: false
})
const scene = new Scene(engine)
scene.useRightHandedSystem = true
scene.enablePhysics(
new Vector3(0, -9.81, 0),
new OimoJSPlugin()
)
const camera = new ArcRotateCamera('camera', -Math.PI / 1.5, Math.PI / 2.2, 15, new Vector3(0, 0, 0), scene)
camera.upperBetaLimit = Math.PI / 2.2
camera.wheelPrecision = 30
camera.panningSensibility = 10
camera.attachControl(ele, true)
camera.setPosition(new Vector3(120, 50, 120))
const createLight = () => {
const light = new HemisphericLight('light',new Vector3(1, 1, 0), scene)
return light
}
const createAxis = () => {
const axisX = MeshBuilder.CreateLines(
'axisX', {
colors: [new Color4(1, 0, 0, 1), new Color4(1, 0, 0, 1)],
points: [new Vector3(0, 0, 0), new Vector3(80, 0, 0)]
},
scene
)
const axisY = MeshBuilder.CreateLines(
'axisY', {
colors: [new Color4(0, 1, 0, 1), new Color4(0, 1, 0, 1) ],
points: [new Vector3(0, 0, 0), new Vector3(0, 80, 0) ]
},
scene
)
const axisZ = MeshBuilder.CreateLines(
'axisZ', {
colors: [new Color4(0, 0, 1, 1), new Color4(0, 0, 1, 1)],
points: [new Vector3(0, 0, 0), new Vector3(0, 0, 80)]
},
scene
)
return [axisX, axisY, axisZ]
}
const createGui = async () => {
adt = AdvancedDynamicTexture.CreateFullscreenUI('UI')
const xBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
xBox.position = new Vector3(80, 0, 0)
const xPanel = new StackPanel()
xPanel.width = '20px'
xPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
xPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
const x = new TextBlock()
x.text = 'X'
x.height = '30px'
x.color = 'red'
adt.addControl(xPanel)
xPanel.addControl(x)
xPanel.linkWithMesh(xBox)
const yBox = MeshBuilder.CreateBox('y', { size: 1 }, scene)
yBox.position = new Vector3(0, 80, 0)
const yPanel = new StackPanel()
yPanel.width = '20px'
yPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
yPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
const y = new TextBlock()
y.text = 'Y'
y.height = '30px'
y.color = 'green'
adt.addControl(yPanel)
yPanel.addControl(y)
yPanel.linkWithMesh(yBox)
const zBox = MeshBuilder.CreateBox('z', { size: 1 }, scene)
zBox.position = new Vector3(0, 0, 80)
const zPanel = new StackPanel()
zPanel.width = '20px'
zPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
zPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
const z = new TextBlock()
z.text = 'Z'
z.height = '30px'
z.color = 'blue'
adt.addControl(zPanel)
zPanel.addControl(z)
zPanel.linkWithMesh(zBox)
}
const createGround = () => {
const mat = new StandardMaterial('ground', scene)
const wood = new Texture('/images/wood.jpg', scene)
mat.diffuseTexture = wood
mat.specularColor = Color3.Black()
const ground = MeshBuilder.CreateBox('box', { size: 200 }, scene)
ground.position.y = 0
ground.position.x = 0
ground.scaling.y = 0.01
ground.material = mat
ground.physicsImpostor = new PhysicsImpostor(
ground,
PhysicsImpostor.BoxImpostor, // 使用盒子形物理代理
{
mass: 0, // 物体的质量设置为 0
restitution: 0.7 // 弹性(或称为恢复系数),范围从 0(无弹性,即完全非弹性碰撞)到 1(完全弹性碰撞)
},
scene
)
}
const randomNumber = (min, max) => {
if (min === max) {
return min
}
const random = Math.random()
return random * (max - min) + min
}
const getPosition = (y) => {
return new Vector3(randomNumber(-50, 50), y, randomNumber(-50, 50))
}
const createSpheres = () => {
const allSphere: any = []
const sphere = MeshBuilder.CreateSphere('s', { diameter: 8 }, scene)
sphere.isVisible = false
for (let index = 0; index < max; index ++) {
const clone = sphere.clone(`index_${index}`)
clone.isVisible = true
const ran = Math.random()
const startY = Math.random() * 10
const dis = y + (ran > 0.5 ? startY : startY * -1)
clone.position = getPosition(dis)
clone.physicsImpostor = new PhysicsImpostor(
clone,
PhysicsImpostor.SphereImpostor, // 使用球形物理代理
{
mass: 50, // 物体的质量设置为 1
restitution: 0.9 // 弹性(或称为恢复系数),范围从 0(无弹性,即完全非弹性碰撞)到 1(完全弹性碰撞)
},
scene
)
// applyImpulse 方法对物理代理对象施加一个冲量
// 这个方法接受两个 Vector3 参数:
// 第一个是冲量的方向和大小
// 第二个是施加冲量的点
// 假设forceVector是 (1, 2, -1)
// contactPoint是 (1, 2, 0),这指定了在 clone 网格对象的局部空间中施加冲量的具体位置
// 冲量大小是根号6 ---> 1^2+2^2+(-1)^2 再开根号
// 冲量的方向是通过反余弦公式得出(arccos(1 / 根号6), arccos(2 / 根号6), arccos(-1 / 根号6))
clone.physicsImpostor.applyImpulse(
new Vector3(0, -20, 0), // 冲量的方向和大小
new Vector3(0, 0, 0) // 在物体上的受力点位置
)
const mat = new StandardMaterial('mat', scene)
mat.diffuseColor = new Color3(0.4, 0.4, 0.4)
mat.specularColor = new Color3(0.4, 0.4, 0.4)
mat.emissiveColor = Color3.Red()
clone.material = mat
allSphere.push(clone)
}
return allSphere
}
const runAnimate = () => {
engine.runRenderLoop(function() {
if (scene && scene.activeCamera) {
scene.render()
fps.value = engine.getFps().toFixed(2)
}
})
}
createGround()
const allSphere = createSpheres()
createAxis()
createGui()
createLight()
runAnimate()
scene.registerBeforeRender(function() {
allSphere.forEach(function(obj) {
if (obj.position.y < -10) {
const ran = Math.random()
const startY = Math.random() * 10
const dis = y + (ran > 0.5 ? startY : startY * -1)
obj.position = getPosition(dis)
// 重置球体的速度
obj.physicsImpostor.setLinearVelocity(new Vector3(0, 0, 0))
}
})
})
return {
scene,
engine
}
}
const destroy = () => {
if (sceneResources) {
sceneResources.engine.stopRenderLoop()
sceneResources.engine.dispose()
sceneResources.scene.dispose()
sceneResources = null
}
if (adt) {
adt.dispose()
adt = null
}
}
onMounted(async() => {
await nextTick()
})
onUnmounted(() => {
destroy()
})
</script>
初使用 Havok 物理引擎
fps: 0
点击运行
使用Havok
<template>
<div>
<div class="flex space-between">
<div>fps: {{ fps }}</div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
</div>
<div>使用Havok</div>
<canvas v-if="isRunning" id="collisionHavok" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
Engine,
Scene,
FreeCamera,
Vector3,
Color3,
HemisphericLight,
MeshBuilder,
StandardMaterial,
Texture,
HavokPlugin,
PhysicsAggregate
} from 'babylonjs'
let sceneResources
const fps = ref(0)
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
sceneResources = await initScene()
} else {
isRunning.value = false
destroy()
}
}
const initScene = async () => {
const ele = document.getElementById("collisionHavok") as any
ele.addEventListener('wheel', function(event) {
// 根据需要处理滚动
// 例如,可以修改相机的半径或角度
event.preventDefault() // 阻止默认滚动行为
})
const engine: any = new Engine(ele, true, {
preserveDrawingBuffer: true,
stencil: true,
disableWebGL2Support: false
})
const scene = new Scene(engine)
const camera = new FreeCamera('camera', new Vector3(0, 5, -10), scene)
camera.setTarget(Vector3.Zero())
camera.attachControl(ele, true)
const havokInstance = await (window as any).HavokPhysics()
const havokPlugin = new HavokPlugin(true, havokInstance)
scene.enablePhysics(new Vector3(0, -9.8, 0), havokPlugin)
const createLight = () => {
const light = new HemisphericLight('light',new Vector3(0, 1, 0), scene)
light.intensity = 0.7
return light
}
const createGround = () => {
const mat = new StandardMaterial('ground', scene)
const wood = new Texture('/images/wood.jpg', scene)
mat.diffuseTexture = wood
mat.specularColor = Color3.Black()
const ground = MeshBuilder.CreateGround('ground', { width: 10, height: 10 }, scene)
ground.material = mat
new PhysicsAggregate(ground, BABYLON.PhysicsShapeType.BOX, {
mass: 0
}, scene)
}
const createSphere = () => {
const sphere = MeshBuilder.CreateSphere('s', { diameter: 2, segments: 32 }, scene)
sphere.position.y = 4
new PhysicsAggregate(sphere, BABYLON.PhysicsShapeType.SPHERE, {
mass: 100,
restitution: 0.75
}, scene)
}
const runAnimate = () => {
engine.runRenderLoop(function() {
if (scene && scene.activeCamera) {
scene.render()
fps.value = engine.getFps().toFixed(2)
}
})
}
createLight()
createGround()
createSphere()
runAnimate()
return {
scene,
engine,
}
}
const destroy = () => {
if (sceneResources) {
sceneResources.engine.stopRenderLoop()
sceneResources.engine.dispose()
sceneResources.scene.dispose()
sceneResources = null
}
}
onMounted(async() => {
await nextTick()
})
onUnmounted(() => {
destroy()
})
</script>
粒子的重力下降与球发生碰撞
fps: 0
点击运行
使用sphere.intersectsMesh(particle, true)检测碰撞
<template>
<div>
<div class="flex space-between">
<div>fps: {{ fps }}</div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
</div>
<div>使用sphere.intersectsMesh(particle, true)检测碰撞</div>
<canvas v-if="isRunning" id="collisionParticle" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import 'babylonjs-loaders'
import {
Engine,
Scene,
ArcRotateCamera,
Vector3,
Color3,
HemisphericLight,
MeshBuilder,
StandardMaterial,
Texture,
SolidParticleSystem
} from 'babylonjs'
let sceneResources
const fps = ref(0)
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
sceneResources = await initScene()
} else {
isRunning.value = false
destroy()
}
}
const initScene = async () => {
const ele = document.getElementById("collisionParticle") as any
ele.addEventListener('wheel', function(event) {
// 根据需要处理滚动
// 例如,可以修改相机的半径或角度
event.preventDefault() // 阻止默认滚动行为
})
const engine: any = new Engine(ele, true, {
preserveDrawingBuffer: true,
stencil: true,
disableWebGL2Support: false
})
const scene = new Scene(engine)
scene.useRightHandedSystem = true
const camera = new ArcRotateCamera('camera', Math.PI / 4, 0, 150, new Vector3(0, 0, 0), scene)
camera.upperBetaLimit = Math.PI / 2.2
camera.wheelPrecision = 30
camera.panningSensibility = 10
camera.attachControl(ele, true)
camera.setPosition(new Vector3(50, 300, 20))
let k = 0.0
let tempPos = Vector3.Zero()
let tempNormal = Vector3.Zero()
let tempDot = 0.0
let sign = 0
let sphereAltitude = 40.0
const speed = 1.9
const cone = 0.3
const gravity = -speed / 100
const restitution = 0.88
const createLight = () => {
const light = new HemisphericLight('light',new Vector3(1, 1, 0), scene)
return light
}
const createGround = () => {
const groundMaterial = new StandardMaterial("groundMaterial", scene)
groundMaterial.diffuseTexture = new Texture('/images/wood.jpg')
const ground = MeshBuilder.CreateGround(
'gd', {
width: 1000.0,
height: 1000.0
},
scene
)
ground.material = groundMaterial
return ground
}
const createSphere = () => {
const matSphere = new StandardMaterial('ms', scene)
matSphere.diffuseColor = Color3.Blue()
const sphere = MeshBuilder.CreateSphere(
's', { diameter: 8 }, scene
)
sphere.material = matSphere
sphere.position.y = 35
return sphere
}
const createBox = () => {
const matBox = new StandardMaterial('ms', scene)
matBox.diffuseColor = Color3.Red()
const box = MeshBuilder.CreateBox(
'b', {
size: 2
},
scene
)
box.material = matBox
return box
}
const createSps = (ground, sphere) => {
/** SolidParticleSystem允许创建由一个或多个基础网格模型组成的粒子云 */
const sps: any = new SolidParticleSystem('sps', scene, {
/** 启用了 particleIntersection,就可以使用任何固体粒子的 intersectsMesh() 方法来检查该粒子是否与目标网格相交 */
particleIntersection: true
})
const box = createBox()
sps.addShape(box, 200) // 添加200个box作为粒子
box.dispose()
const mesh = sps.buildMesh() // 调用 buildMesh() 方法来构建 SPS 网格
mesh.position.y = 80.0
mesh.position.x = -70.0
sphereAltitude = mesh.position.y / 2.0
sphere.position.y = sphereAltitude
sps.isAlwaysVisible = true
sps.initParticles = () => {
for (let p = 0; p < sps.nbParticles; p++) {
sps.recycleParticle(sps.particles[p])
}
}
// recycleParticle方法允许设置一个粒子以便于回收利用
// 可以帮助提高性能,尤其是在处理大量粒子时
// 在updateParticle方法中定义粒子的行为,并在适当的时候调用recycleParticle来回收粒子
sps.recycleParticle = (particle: any) => {
particle.position.x = 0
particle.position.y = 0
particle.position.z = 0
particle.velocity.x = Math.random() * speed
particle.velocity.y = (Math.random() - 0.3) * cone * speed
particle.velocity.z = (Math.random() - 0.5) * cone * speed
particle.rotation.x = Math.random() * Math.PI
particle.rotation.y = Math.random() * Math.PI
particle.rotation.z = Math.random() * Math.PI
particle.color.r = 0.0
particle.color.g = 1.0
particle.color.b = 0.0
particle.color.a = 1.0
}
sps.updateParticle = (particle: any) => {
if (particle.position.y + mesh.position.y < ground.position.y) {
particle.color.r = 0.0
particle.color.g = 1.0
particle.color.b = 0.0
sps.recycleParticle(particle)
}
particle.velocity.y += gravity
particle.position.addInPlace(particle.velocity) // 更新粒子新位置
// 使得每个粒子在三个轴(x、y、z)上以不同的速度和方向旋转,从而创造出更加复杂和动态的视觉效果。
// 通过交替改变旋转方向,粒子的旋转看起来更加自然和随机。
sign = particle.idx % 2 === 0 ? 1 : -1
particle.rotation.z += 0.1 * sign
particle.rotation.x += 0.05 * sign
particle.rotation.y += 0.008 * sign
// 判断发生碰撞
if (particle.intersectsMesh(sphere)) {
// 将粒子的位置向量与网格(mesh)的位置向量相加,结果存储在 tempPos 变量中,这样 tempPos 就包含了粒子的世界位置
particle.position.addToRef(mesh.position, tempPos)
// 从 tempPos 中减去球体的位置向量,结果存储在 tempNormal 变量中,这样 tempNormal 就包含了从球体中心指向粒子的向量,即球体的法线
tempPos.subtractToRef(sphere.position, tempNormal)
// 将 tempNormal 向量标准化(即长度变为1),这样它就成为了一个单位法线向量
tempNormal.normalize()
// 计算粒子速度向量与球体法线向量的点积,结果存储在 tempDot 变量中
tempDot = Vector3.Dot(particle.velocity, tempNormal)
/**
在物理学中,当处理两个物体之间的碰撞时,特别是涉及到法线分量的速度时,一个常见的方法是使用分离公式。
这个公式是从动量守恒和能量守恒原理推导出来的。
给定:
v 是粒子的原始速度向量。
n 是碰撞物体的单位法线向量。
v_new 是碰撞后粒子的速度向量。
速度向量 v 可以分解为两个分量:
平行于法线 n 的分量:v∥ = (v⋅n)n -------- v⋅n 是点积
垂直于法线 n 的分量:v⊥ = v − v∥
当粒子与物体碰撞时,垂直于法线的分量保持不变,而平行于法线的分量被反转并根据恢复系数 e 进行缩放。
恢复系数 e 描述了碰撞的弹性程度,其中 e=1 表示完全弹性碰撞,而 e=0 表示完全非弹性碰撞。
碰撞后的速度向量 v_new 由下式给出:
v_new = v⊥ - e(v⋅n)n
*/
/**
1、法线向量的计算:
当粒子与球体碰撞时,首先计算从球体中心指向粒子的向量(tempNormal),并将其标准化为单位法线向量。
这个向量表示了碰撞点的法线方向。
2、速度在法线方向上的分量:
接下来,计算粒子速度向量(particle.velocity)与单位法线向量(tempNormal)的点积(tempDot)。
点积的结果给出了速度向量在法线方向上的分量的大小(正值或负值,取决于速度向量与法线向量的夹角是锐角还是钝角)。
3、速度向量的更新:
根据弹性碰撞的原理,速度向量在法线方向上的分量会完全反向,并且大小可能保持不变(在完全弹性碰撞中)。
因此,从原始速度向量中减去其在法线方向上的分量(乘以2并反向,以符合反射定律),然后加上这个反向的分量乘以单位法线向量的相应分量。
这样做实际上是将速度向量在其法线分量上进行了反射。
数学上,这可以表示为:
new_velocity_component=−original_velocity_component+2×dot_product×normal_component
original_velocity_component 是速度向量在x、y或z方向上的原始分量
dot_product 是速度向量与法线向量的点积
normal_component 是单位法线向量在x、y或z方向上的分量
4、恢复系数的应用:
最后,将更新后的速度向量乘以恢复系数(restitution),以模拟碰撞后的能量损失。
恢复系数是一个介于0和1之间的数,值越接近1表示碰撞越弹性。
*/
/**
假设一个图中
有一个粒子(P)和一个球体(S)。粒子的速度向量(V)与球体的表面相交于点C,形成一个入射角θ。
从球体中心S到粒子P的向量(SC)被标准化为单位法线向量(N)。
速度向量V与法线向量N的点积给出了速度在法线方向上的分量。
新的速度向量(V')是通过将速度向量V在其法线分量上进行反射得到的。注意,这里的反射是基于二维平面的,但在三维中原理相同。
最后,新的速度向量V'被乘以恢复系数,以模拟碰撞后的能量损失。
*/
particle.velocity.x = -particle.velocity.x + 2.0 * tempDot * tempNormal.x
particle.velocity.y = -particle.velocity.y + 2.0 * tempDot * tempNormal.y
particle.velocity.z = -particle.velocity.z + 2.0 * tempDot * tempNormal.z
// 将粒子速度向量乘以恢复系数(restitution),这是弹性碰撞的一个参数,用于模拟碰撞后的反弹效果
particle.velocity.scaleInPlace(restitution)
particle.color.r = 1.0
particle.color.g = 0.0
particle.color.b = 0.0
}
}
return sps
}
const runAnimate = () => {
engine.runRenderLoop(function() {
if (scene && scene.activeCamera) {
scene.render()
fps.value = engine.getFps().toFixed(2)
}
})
}
createLight()
const ground = createGround()
const sphere = createSphere()
const sps = createSps(ground, sphere)
sps.initParticles()
runAnimate()
scene.registerBeforeRender(function() {
sps.particles.forEach(particle => {
if (sphere.intersectsMesh(particle, true)) {
console.log('collided')
}
})
sps.setParticles()
sphere.position.x = 20.0 * Math.sin(k)
sphere.position.z = 10.0 * Math.sin(k * 6.0)
sphere.position.y = 5.0 * Math.sin(k * 10) + sphereAltitude
k += 0.01
})
return {
scene,
engine,
}
}
const destroy = () => {
if (sceneResources) {
sceneResources.engine.stopRenderLoop()
sceneResources.engine.dispose()
sceneResources.scene.dispose()
sceneResources = null
}
}
onMounted(async() => {
await nextTick()
})
onUnmounted(() => {
destroy()
})
</script>
推倒积木 -1
fps: 0
点击运行
使用Havok
<template>
<div>
<div class="flex space-between">
<div>fps: {{ fps }}</div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
</div>
<div>使用Havok</div>
<canvas v-if="isRunning" id="buildingBlock1" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
Engine,
Scene,
ArcRotateCamera,
Vector3,
Color3,
Color4,
HemisphericLight,
MeshBuilder,
StandardMaterial,
Texture,
HavokPlugin,
Matrix,
PhysicsAggregate,
PhysicsShapeType,
PointerEventTypes
} from 'babylonjs'
import {
AdvancedDynamicTexture,
StackPanel,
Control,
TextBlock,
} from 'babylonjs-gui'
let sceneResources, adt
const fps = ref(0)
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
sceneResources = await initScene()
} else {
isRunning.value = false
destroy()
}
}
const initScene = async () => {
const ele = document.getElementById("buildingBlock1") as any
ele.addEventListener('wheel', function(event) {
// 根据需要处理滚动
// 例如,可以修改相机的半径或角度
event.preventDefault() // 阻止默认滚动行为
})
const engine: any = new Engine(ele, true, {
preserveDrawingBuffer: true,
stencil: true,
disableWebGL2Support: false
})
const scene = new Scene(engine)
scene.useRightHandedSystem = true
const camera = new ArcRotateCamera('camera', 0, 0, 0, new Vector3(0, 0, 0), scene)
camera.upperBetaLimit = Math.PI / 2.2
camera.wheelPrecision = 30
camera.panningSensibility = 10
camera.attachControl(ele, true)
camera.setPosition(new Vector3(7, 7, 0))
const havokInstance = await (window as any).HavokPhysics()
const havokPlugin = new HavokPlugin(true, havokInstance)
scene.enablePhysics(new Vector3(0, -9.8, 0), havokPlugin)
const createAxis = () => {
const axisX = MeshBuilder.CreateLines(
'axisX', {
colors: [new Color4(1, 0, 0, 1), new Color4(1, 0, 0, 1)],
points: [new Vector3(0, 0, 0), new Vector3(80, 0, 0)]
},
scene
)
const axisY = MeshBuilder.CreateLines(
'axisY', {
colors: [new Color4(0, 1, 0, 1), new Color4(0, 1, 0, 1) ],
points: [new Vector3(0, 0, 0), new Vector3(0, 80, 0) ]
},
scene
)
const axisZ = MeshBuilder.CreateLines(
'axisZ', {
colors: [new Color4(0, 0, 1, 1), new Color4(0, 0, 1, 1)],
points: [new Vector3(0, 0, 0), new Vector3(0, 0, 80)]
},
scene
)
return [axisX, axisY, axisZ]
}
const createGui = async () => {
adt = AdvancedDynamicTexture.CreateFullscreenUI('UI')
const xBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
xBox.position = new Vector3(80, 0, 0)
const xPanel = new StackPanel()
xPanel.width = '20px'
xPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
xPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
const x = new TextBlock()
x.text = 'X'
x.height = '30px'
x.color = 'red'
adt.addControl(xPanel)
xPanel.addControl(x)
xPanel.linkWithMesh(xBox)
const yBox = MeshBuilder.CreateBox('y', { size: 1 }, scene)
yBox.position = new Vector3(0, 80, 0)
const yPanel = new StackPanel()
yPanel.width = '20px'
yPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
yPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
const y = new TextBlock()
y.text = 'Y'
y.height = '30px'
y.color = 'green'
adt.addControl(yPanel)
yPanel.addControl(y)
yPanel.linkWithMesh(yBox)
const zBox = MeshBuilder.CreateBox('z', { size: 1 }, scene)
zBox.position = new Vector3(0, 0, 80)
const zPanel = new StackPanel()
zPanel.width = '20px'
zPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
zPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
const z = new TextBlock()
z.text = 'Z'
z.height = '30px'
z.color = 'blue'
adt.addControl(zPanel)
zPanel.addControl(z)
zPanel.linkWithMesh(zBox)
}
const createLight = () => {
const light = new HemisphericLight('light',new Vector3(0, 1, 0), scene)
light.intensity = 0.7
return light
}
const createGround = () => {
const mat = new StandardMaterial('ground', scene)
const wood = new Texture('/images/wood.jpg', scene)
mat.diffuseTexture = wood
mat.specularColor = Color3.Black()
const ground = MeshBuilder.CreateGround('ground', { width: 10, height: 10 }, scene)
ground.material = mat
new PhysicsAggregate(ground, PhysicsShapeType.BOX, {
mass: 0
}, scene)
}
const assetsBody: any = []
const createBlock = (x, y, z, i, color) => {
const block: any = MeshBuilder.CreateBox('block_' + i, { width: 0.2, height: 1.8, depth: 0.2 }, scene)
block.position.set(x, y, z)
block.checkCollisions = true
block.material = new StandardMaterial('blockMat', scene)
block.material.diffuseColor = color
const aggregate = new PhysicsAggregate(block, PhysicsShapeType.BOX, {
mass: 10,
restitution: 0.25
}, scene)
aggregate.body.disablePreStep = false
return {
asset: block,
aggregate: aggregate
}
}
const createSphere = () => {
const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 1 }, scene)
sphere.position.set(2, 0.5, 0)
sphere.checkCollisions = true
const aggregate = new PhysicsAggregate(sphere, PhysicsShapeType.SPHERE, {
mass: 10,
restitution: 0.25, // 弹性系数
friction: 0.5 // 摩擦力
}, scene)
aggregate.body.disablePreStep = false
return {
asset: sphere,
aggregate: aggregate
}
}
const runAnimate = () => {
engine.runRenderLoop(function() {
if (scene && scene.activeCamera) {
scene.render()
fps.value = engine.getFps().toFixed(2)
}
})
}
createAxis()
createGui()
createLight()
createGround()
runAnimate()
const sphere: any = createSphere()
assetsBody.push(sphere)
for (let i = 0; i < 5; i++) {
const block = createBlock(i * -1, 0.9, 0, i, Color3.Red())
assetsBody.push(block)
}
scene.onPointerObservable.add((pointerInfo) => {
if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
// 使用 scene.pickWithRay 获取鼠标点击的射线与球体的交点
// 如果点击到球体,pickResult.hit 为 true,pickResult.pickedPoint 是点击位置
const pickResult: any = scene.pickWithRay(scene.createPickingRay(scene.pointerX, scene.pointerY, Matrix.Identity(), camera))
if (pickResult.hit && pickResult.pickedMesh === sphere.asset) {
// 计算施加力的方向:从球体中心到点击位置的方向
const impulseDirection = pickResult.pickedPoint.subtract(sphere.asset.position).normalize()
// 力的大小
const impulse = impulseDirection.scale(-20)
sphere.aggregate.body.applyImpulse(impulse, pickResult.pickedPoint)
}
}
})
return {
scene,
engine,
}
}
const destroy = () => {
if (sceneResources) {
sceneResources.engine.stopRenderLoop()
sceneResources.engine.dispose()
sceneResources.scene.dispose()
sceneResources = null
}
if (adt) {
adt.dispose()
adt = null
}
}
onMounted(async() => {
await nextTick()
})
onUnmounted(() => {
destroy()
})
</script>
模拟台球 -1
fps: 0
点击运行
使用Havok
力度:0
<template>
<div>
<div class="flex space-between">
<div>fps: {{ fps }}</div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
</div>
<div>使用Havok</div>
<div class="flex space-between">
<div>力度:{{ impulse }}</div>
<div>{{ gameOver }}</div>
</div>
<canvas v-if="isRunning" id="billiardBall1" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
Engine,
Scene,
ArcRotateCamera,
Vector3,
Color3,
Color4,
StandardMaterial,
HemisphericLight,
MeshBuilder,
HavokPlugin,
PhysicsAggregate,
PhysicsShapeType,
PointerEventTypes,
Matrix,
Animation,
} from 'babylonjs'
import {
AdvancedDynamicTexture,
StackPanel,
Control,
TextBlock,
} from 'babylonjs-gui'
let sceneResources, adt
let cueAnimation: any = null
let timer: any = null
let invert = 1
let isAiming = false
let balls: any = []
let pockets: any = []
let clickBall: any = null
const impulse = ref(0)
const gameOver = ref('')
const fps = ref(0)
const isRunning = ref(false)
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
sceneResources = await initScene()
gameOver.value = ''
} else {
isRunning.value = false
destroy()
}
}
const initScene = async () => {
const ele = document.getElementById("billiardBall1") as any
ele.addEventListener('wheel', function(event) {
// 根据需要处理滚动
// 例如,可以修改相机的半径或角度
event.preventDefault() // 阻止默认滚动行为
})
const engine: any = new Engine(ele, true, {
preserveDrawingBuffer: true,
stencil: true,
disableWebGL2Support: false
})
const scene = new Scene(engine)
scene.useRightHandedSystem = true
const camera = new ArcRotateCamera('camera', 0, 0, 0, new Vector3(0, 0, 0), scene)
camera.upperBetaLimit = Math.PI / 2.2
camera.wheelPrecision = 30
camera.panningSensibility = 10
camera.attachControl(ele, true)
camera.setPosition(new Vector3(0, 0, 10))
const havokInstance = await (window as any).HavokPhysics()
const havokPlugin = new HavokPlugin(true, havokInstance)
scene.enablePhysics(new Vector3(0, -9.8, 0), havokPlugin)
const createAxis = () => {
const axisX = MeshBuilder.CreateLines(
'axisX', {
colors: [new Color4(1, 0, 0, 1), new Color4(1, 0, 0, 1)],
points: [new Vector3(0, 0, 0), new Vector3(80, 0, 0)]
},
scene
)
const axisY = MeshBuilder.CreateLines(
'axisY', {
colors: [new Color4(0, 1, 0, 1), new Color4(0, 1, 0, 1) ],
points: [new Vector3(0, 0, 0), new Vector3(0, 80, 0) ]
},
scene
)
const axisZ = MeshBuilder.CreateLines(
'axisZ', {
colors: [new Color4(0, 0, 1, 1), new Color4(0, 0, 1, 1)],
points: [new Vector3(0, 0, 0), new Vector3(0, 0, 80)]
},
scene
)
return [axisX, axisY, axisZ]
}
const createGui = async () => {
adt = AdvancedDynamicTexture.CreateFullscreenUI('UI')
const xBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
xBox.position = new Vector3(80, 0, 0)
const xPanel = new StackPanel()
xPanel.width = '20px'
xPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
xPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
const x = new TextBlock()
x.text = 'X'
x.height = '30px'
x.color = 'red'
adt.addControl(xPanel)
xPanel.addControl(x)
xPanel.linkWithMesh(xBox)
const yBox = MeshBuilder.CreateBox('y', { size: 1 }, scene)
yBox.position = new Vector3(0, 80, 0)
const yPanel = new StackPanel()
yPanel.width = '20px'
yPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
yPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
const y = new TextBlock()
y.text = 'Y'
y.height = '30px'
y.color = 'green'
adt.addControl(yPanel)
yPanel.addControl(y)
yPanel.linkWithMesh(yBox)
const zBox = MeshBuilder.CreateBox('z', { size: 1 }, scene)
zBox.position = new Vector3(0, 0, 80)
const zPanel = new StackPanel()
zPanel.width = '20px'
zPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
zPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
const z = new TextBlock()
z.text = 'Z'
z.height = '30px'
z.color = 'blue'
adt.addControl(zPanel)
zPanel.addControl(z)
zPanel.linkWithMesh(zBox)
}
const createLight = () => {
const light = new HemisphericLight('light',new Vector3(0, 1, 0), scene)
light.intensity = 0.7
return light
}
// 创建台球桌
const createTable = () => {
const table = MeshBuilder.CreateBox('table', {
width: 10,
height: 0.5,
depth: 5
}, scene)
table.position.y = -0.25
new PhysicsAggregate(table, PhysicsShapeType.BOX, {
mass: 0,
friction: 0.5,
restitution: 0.1
}, scene)
}
// 创建台球桌墙
const createTableWall = () => {
const wallPositions = [
{ width: 0.2, height: 0.2, depth: 4.2, x: -5, y: 0.1, z: 0 }, // 左墙
{ width: 0.2, height: 0.2, depth: 4.2, x: 5, y: 0.1, z: 0 }, // 右墙
{ width: 4.3, height: 0.2, depth: 0.2, x: -2.45, y: 0.1, z: 2.5 }, // 下墙-左
{ width: 4.3, height: 0.2, depth: 0.2, x: 2.45, y: 0.1, z: 2.5 }, // 下墙-右
{ width: 4.3, height: 0.2, depth: 0.2, x: -2.45, y: 0.1, z: -2.5 }, // 上墙-左
{ width: 4.3, height: 0.2, depth: 0.2, x: 2.45, y: 0.1, z: -2.5 }, // 上墙-右
]
for (let i = 0; i < wallPositions.length; i++) {
const info = wallPositions[i]
const wall: any = MeshBuilder.CreateBox('wall' + i, {
width: info.width,
height: info.height,
depth: info.depth
}, scene)
wall.position = new Vector3(info.x, info.y, info.z)
wall.material = new StandardMaterial('wallMaterial' + i, scene)
wall.material.diffuseColor = new Color3(0.6, 0.2, 0.4)
new PhysicsAggregate(wall, PhysicsShapeType.BOX, {
mass: 0,
friction: 0.5,
restitution: 0.1
}, scene)
}
}
// 创建球袋
const createPocket = () => {
const pocketPositions = [
new Vector3(-4.9, 0, -2.4),
new Vector3(4.9, 0, -2.4),
new Vector3(-4.9, 0, 2.4),
new Vector3(4.9, 0, 2.4),
new Vector3(0, 0, -2.4),
new Vector3(0, 0, 2.4)
]
for (let i = 0; i < pocketPositions.length; i++) {
const pocket: any = MeshBuilder.CreateCylinder('pocket' + i, {
height: 0.5,
diameter: 0.7,
tessellation: 64
}, scene)
pocket.position = pocketPositions[i]
pocket.position.y = -0.23
pocket.material = new StandardMaterial('pocketMaterial' + i, scene)
pocket.material.diffuseColor = new Color3(0.1, 0.1, 0.1)
pockets.push(pocket)
}
}
// 创建台球
const createBall = () => {
const ballColors = [
new Color3(1, 1, 1), // 白球
new Color3(0.8, 0.1, 0.1), // 红球
new Color3(0.1, 0.8, 0.1), // 绿球
new Color3(0.1, 0.1, 0.8), // 蓝球
new Color3(0.8, 0.8, 0.1), // 黄球
new Color3(0.8, 0.1, 0.8), // 紫球
new Color3(0.1, 0.8, 0.8), // 青球
new Color3(0.5, 0.5, 0.5) // 黑球
]
for (let i = 0; i < 8; i++) {
const ball: any = MeshBuilder.CreateSphere('ball' + i, {
diameter: 0.3
}, scene)
ball.position.x = Math.cos(i * Math.PI / 4) * 1.5
ball.position.z = Math.sin(i * Math.PI / 4) * 1.5
ball.position.y = 0.25
ball.material = new StandardMaterial('ballMaterial' + i, scene)
ball.material.diffuseColor = ballColors[i]
ball.customAggregate = new PhysicsAggregate(ball, PhysicsShapeType.SPHERE, {
mass: 0.5,
friction: 0.1,
restitution: 0.8
}, scene)
balls.push(ball)
}
}
// 创建球杆
const createCue = () => {
const cue: any = MeshBuilder.CreateCylinder('cue', {
height: 6,
diameterTop: 0.1,
diameterBottom: 0.2,
tessellation: 64
}, scene)
cue.rotation = new Vector3(Math.PI / 2, 0, 0)
cue.material = new StandardMaterial('cueMaterial', scene)
cue.material.diffuseColor = new Color3(0.8, 0.6, 0.4)
const cueParent = MeshBuilder.CreateBox('box', {
width: 0.5,
height: 0.5,
depth: 0.5
}, scene)
cueParent.position.set(0, 2, 0)
cue.parent = cueParent
cue.isVisible = false // 初始隐藏
cueParent.isVisible = false // 初始隐藏
return {
cueParent,
cue
}
}
// 创建动画,90 / 60 = 1.5s,90帧,60帧每秒,1.5秒完成动画
const createCueAnimation = (cueTemp, direction) => {
const keys: any = []
keys.push({
frame: 0,
value: cueTemp.position.clone()
})
keys.push({
frame: 45,
value: cueTemp.position.add(direction.scale(1)) // 沿着direction方向前进 1 单位
})
keys.push({
frame: 90,
value: cueTemp.position // 后退 1 单位(即返回原位置)
})
if(!cueAnimation) { // 防止重复new
cueAnimation = new Animation(
'boxAnimation',
'position',
60, // 帧率
Animation.ANIMATIONTYPE_VECTOR3,
Animation.ANIMATIONLOOPMODE_CYCLE
)
cueTemp.animations.push(cueAnimation)
}
cueAnimation.setKeys(keys)
scene.beginAnimation(cueTemp, 0, 90, true) // 设置true为循环播放
}
const addImpulse = () => {
timer = requestAnimationFrame(addImpulse)
if (impulse.value >= 2.5) {
invert = -1
} else if (impulse.value <= 0) {
invert = 1
}
impulse.value += 0.05 * invert
impulse.value = Number(impulse.value.toFixed(2))
}
const runAnimate = () => {
engine.runRenderLoop(function() {
if (scene && scene.activeCamera) {
scene.render()
fps.value = engine.getFps().toFixed(2)
}
})
}
createAxis()
createGui()
createLight()
createTable()
createBall()
createPocket()
createTableWall()
const { cue, cueParent } = createCue()
runAnimate()
scene.onPointerObservable.add(pointerInfo => {
if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
const pickedMesh = pointerInfo.pickInfo?.pickedMesh
const hit = pointerInfo.pickInfo?.hit
if (hit && pickedMesh?.name?.includes('ball')) {
isAiming = true
clickBall = pickedMesh
// 禁止camera转动
camera.detachControl()
// 点击到球体的xyz位置
const clickPosition = pickedMesh.position
// 相机位置
const cameraPosition = camera.position.clone()
// 设置球杆的位置
const middlePosition = cameraPosition.add(clickPosition).scale(0.5)
cueParent.position = middlePosition
// lookAt使得球杆对着球体
cueParent.lookAt(clickPosition)
// 计算看向球体的方向向量,click减去camera,方向指向click
const direction = clickPosition.subtract(cameraPosition).normalize()
cue.isVisible = true
createCueAnimation(cueParent, direction)
impulse.value = 0
addImpulse()
}
}
if (pointerInfo.type === PointerEventTypes.POINTERUP) {
const pick: any = pointerInfo.pickInfo?.pickedMesh
const hit = pointerInfo.pickInfo?.hit
if (isAiming && hit && pick?.name === 'cue') {
isAiming = false
// 使用 scene.pickWithRay 获取鼠标点击的射线与球体的交点
const x = scene.pointerX
const y = scene.pointerY
const m = Matrix.Identity()
const pickResult = scene.pickWithRay(scene.createPickingRay(x, y, m, camera))
// 计算施加冲量的方向:从球体中心到点击位置的方向
const impulseDirection = pickResult?.pickedPoint?.subtract(clickBall.position).normalize()
const resultImpulse = impulseDirection?.scale(-impulse.value)
clickBall.customAggregate.body.applyImpulse(resultImpulse, pickResult?.pickedPoint)
}
// 开启camera转动
camera.attachControl()
cue.isVisible = false
cancelAnimationFrame(timer)
timer = null
}
})
// 游戏逻辑
scene.registerBeforeRender(function() {
// 检查球是否落入球袋
for (let i = 0; i < pockets.length; i++) {
for (let j = 0; j < balls.length; j++) {
const distance = BABYLON.Vector3.Distance(pockets[i].position, balls[j].position)
if (distance < 0.5) {
balls[j].dispose()
balls.splice(j, 1)
j--
}
}
}
// 检查游戏是否结束
if (balls.length === 0) {
gameOver.value = '游戏结束'
}
})
return {
scene,
engine,
}
}
const destroy = () => {
if (sceneResources) {
sceneResources.engine.stopRenderLoop()
sceneResources.engine.dispose()
sceneResources.scene.dispose()
sceneResources = null
}
if (adt) {
adt.dispose()
adt = null
}
balls = []
pockets = []
cueAnimation = null
clickBall = null
impulse.value = 0
}
onMounted(async() => {
await nextTick()
})
onUnmounted(() => {
destroy()
})
</script>