demo案例
new Ammo.btSoftBodyHelpers()
是 Ammo.js 中的一个构造函数,用于创建软体物体的辅助对象,提供了一些方法来创建软体物体。以下是它的一些重要信息:
-
入参:通常不需要传入参数。
-
出参:创建的新的软体辅助对象。
-
属性:该构造函数返回的对象通常没有额外的属性,它主要用于提供方法来创建软体物体。
-
方法:
CreateFromTriMesh(worldInfo, vertices, indices, numIndices, randomizeConstraints)
:基于三角网格创建软体物体。它接受以下参数:worldInfo
:物理世界信息对象。vertices
:顶点数组。indices
:索引数组。numIndices
:索引数量。randomizeConstraints
:是否随机化约束。
- 其他用于创建软体物体的方法。
new Ammo.btTransform
是 Ammo.js 中的一个构造函数,用于创建表示刚体变换的对象。以下是它的一些重要信息:
-
入参:通常不需要传入参数。如果需要初始化变换矩阵,可以传入一个矩阵对象。
-
出参:创建的新的变换对象。
-
属性:
origin
:表示变换的位置的向量。rotation
:表示变换的旋转的四元数。
-
方法:
setIdentity()
:将变换设置为单位变换。setOrigin(origin)
:设置变换的位置。setRotation(rotation)
:设置变换的旋转。- 其他用于获取和设置变换的位置和旋转的方法。
Ammo.js 物理软体体积的核心代码是在 createSoftVolume
函数中。具体来说,以下是该函数的关键部分:
function createSoftVolume( bufferGeom, mass, pressure ) {
processGeometry( bufferGeom );
const volume = new THREE.Mesh( bufferGeom, new THREE.MeshPhongMaterial( { color: 0xFFFFFF } ) );
volume.castShadow = true;
volume.receiveShadow = true;
volume.frustumCulled = false;
scene.add( volume );
// 创建软体体积物理对象
const volumeSoftBody = softBodyHelpers.CreateFromTriMesh(
physicsWorld.getWorldInfo(),
bufferGeom.ammoVertices,
bufferGeom.ammoIndices,
bufferGeom.ammoIndices.length / 3,
true );
const sbConfig = volumeSoftBody.get_m_cfg();
sbConfig.set_viterations( 40 );
sbConfig.set_piterations( 40 );
// 设置软体的碰撞参数
sbConfig.set_collisions( 0x11 );
// 设置摩擦、阻尼和压力
sbConfig.set_kDF( 0.1 );
sbConfig.set_kDP( 0.01 );
sbConfig.set_kPR( pressure );
// 设置软体的刚度
volumeSoftBody.get_m_materials().at( 0 ).set_m_kLST( 0.9 );
volumeSoftBody.get_m_materials().at( 0 ).set_m_kAST( 0.9 );
volumeSoftBody.setTotalMass( mass, false );
Ammo.castObject( volumeSoftBody, Ammo.btCollisionObject ).getCollisionShape().setMargin( margin );
physicsWorld.addSoftBody( volumeSoftBody, 1, - 1 );
volume.userData.physicsBody = volumeSoftBody;
// 禁用去激活
volumeSoftBody.setActivationState( 4 );
softBodies.push( volume );
}
这段代码首先调用 processGeometry
函数对几何体进行处理,然后创建一个 THREE.Mesh
对象表示软体体积的图形表示。接着,通过 softBodyHelpers.CreateFromTriMesh
方法创建了一个 Ammo.js 的软体体积物理对象,设置了软体的各种物理参数,最后将软体体积物理对象添加到物理世界中。
demo 源码
<html lang="en">
<head>
<title>Ammo.js软体体积演示</title> <!-- 设置页面标题 -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>
body {
color: #333;
}
</style>
</head>
<body>
<div id="info">
Ammo.js物理软体体积演示<br/> <!-- 页面顶部信息 -->
点击以抛出一个球 <!-- 提示用户点击以抛出一个球 -->
</div>
<div id="container"></div>
<script src="jsm/libs/ammo.wasm.js"></script> <!-- 引入Ammo.js库 -->
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js", <!-- 引入Three.js库 -->
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three'; <!-- 导入Three.js库 -->
import Stats from 'three/addons/libs/stats.module.js'; <!-- 导入性能统计库Stats.js -->
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; <!-- 导入OrbitControls控制器 -->
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; <!-- 导入BufferGeometryUtils工具库 -->
// 图形变量
let container, stats; <!-- 定义容器和性能统计变量 -->
let camera, controls, scene, renderer; <!-- 定义相机、控制器、场景和渲染器变量 -->
let textureLoader; <!-- 纹理加载器变量 -->
const clock = new THREE.Clock(); <!-- 时钟变量 -->
let clickRequest = false; <!-- 点击请求标志 -->
const mouseCoords = new THREE.Vector2(); <!-- 鼠标坐标变量 -->
const raycaster = new THREE.Raycaster(); <!-- 射线投射器变量 -->
const ballMaterial = new THREE.MeshPhongMaterial( { color: 0x202020 } ); <!-- 球体材质 -->
const pos = new THREE.Vector3(); <!-- 位置向量 -->
const quat = new THREE.Quaternion(); <!-- 四元数变量 -->
// 物理变量
const gravityConstant = - 9.8; <!-- 重力常数 -->
let physicsWorld; <!-- 物理世界变量 -->
const rigidBodies = []; <!-- 刚体数组 -->
const softBodies = []; <!-- 软体数组 -->
const margin = 0.05; <!-- 边缘间距 -->
let transformAux1; <!-- 辅助变换对象 -->
let softBodyHelpers; <!-- 软体帮助对象 -->
Ammo().then( function ( AmmoLib ) { <!-- 异步加载Ammo.js库 -->
Ammo = AmmoLib;
init(); <!-- 初始化 -->
animate(); <!-- 动画循环 -->
} );
function init() { <!-- 初始化函数 -->
initGraphics(); <!-- 初始化图形部分 -->
initPhysics(); <!-- 初始化物理部分 -->
createObjects(); <!-- 创建物体 -->
initInput(); <!-- 初始化输入事件 -->
}
function initGraphics() { <!-- 初始化图形函数 -->
container = document.getElementById( 'container' ); <!-- 获取容器元素 -->
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.2, 2000 ); <!-- 创建透视相机 -->
scene = new THREE.Scene(); <!-- 创建场景 -->
scene.background = new THREE.Color( 0xbfd1e5 ); <!-- 设置场景背景色 -->
camera.position.set( - 7, 5, 8 ); <!-- 设置相机位置 -->
renderer = new THREE.WebGLRenderer( { antialias: true } ); <!-- 创建WebGL渲染器 -->
renderer.setPixelRatio( window.devicePixelRatio ); <!-- 设置像素比 -->
renderer.setSize( window.innerWidth, window.innerHeight ); <!-- 设置渲染器大小 -->
renderer.shadowMap.enabled = true; <!-- 开启阴影映射 -->
container.appendChild( renderer.domElement ); <!-- 将渲染器添加到容器中 -->
controls = new OrbitControls( camera, renderer.domElement ); <!-- 创建轨道控制器 -->
controls.target.set( 0, 2, 0 ); <!-- 设置控制器目标 -->
controls.update(); <!-- 更新控制器状态 -->
textureLoader = new THREE.TextureLoader(); <!-- 创建纹理加载器对象 -->
const ambientLight = new THREE.AmbientLight( 0xbbbbbb ); <!-- 创建环境光 -->
scene.add( ambientLight ); <!-- 将环境光添加到场景中 -->
const light = new THREE.DirectionalLight( 0xffffff, 3 ); <!-- 创建方向光 -->
light.position.set( - 10, 10, 5 ); <!-- 设置光源位置 -->
light.castShadow = true; <!-- 开启阴影 -->
const d = 20;
light.shadow.camera.left = - d; <!-- 设置阴影相机左边界 -->
light.shadow.camera.right = d; <!-- 设置阴影相机右边界 -->
light.shadow.camera.top = d; <!-- 设置阴影相机上边界 -->
light.shadow.camera.bottom = - d; <!-- 设置阴影相机下边界 -->
light.shadow.camera.near = 2; <!-- 设置阴影相机近裁剪面 -->
light.shadow.camera.far = 50; <!-- 设置阴影相机远裁剪面 -->
light.shadow.mapSize.x = 1024; <!-- 设置阴影贴图大小 -->
light.shadow.mapSize.y = 1024; <!-- 设置阴影贴图大小 -->
scene.add( light ); <!-- 将光源添加到场景中 -->
stats = new Stats(); <!-- 创建性能统计对象 -->
stats.domElement.style.position = 'absolute'; <!-- 设置性能统计元素位置 -->
stats.domElement.style.top = '0px'; <!-- 设置性能统计元素位置 -->
container.appendChild( stats.domElement ); <!-- 将性能统计元素添加到容器中 -->
window.addEventListener( 'resize', onWindowResize ); <!-- 监听窗口大小变化事件 -->
}
function initPhysics() { <!-- 初始化物理
函数 -->
// 物理配置
const collisionConfiguration = new Ammo.btSoftBodyRigidBodyCollisionConfiguration(); <!-- 创建碰撞配置对象 -->
const dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration ); <!-- 创建碰撞分发器对象 -->
const broadphase = new Ammo.btDbvtBroadphase(); <!-- 创建Broadphase对象 -->
const solver = new Ammo.btSequentialImpulseConstraintSolver(); <!-- 创建约束求解器对象 -->
const softBodySolver = new Ammo.btDefaultSoftBodySolver(); <!-- 创建软体求解器对象 -->
physicsWorld = new Ammo.btSoftRigidDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration, softBodySolver ); <!-- 创建软硬体动力学世界对象 -->
physicsWorld.setGravity( new Ammo.btVector3( 0, gravityConstant, 0 ) ); <!-- 设置重力 -->
physicsWorld.getWorldInfo().set_m_gravity( new Ammo.btVector3( 0, gravityConstant, 0 ) ); <!-- 设置世界重力 -->
transformAux1 = new Ammo.btTransform(); <!-- 创建辅助变换对象 -->
softBodyHelpers = new Ammo.btSoftBodyHelpers(); <!-- 创建软体帮助对象 -->
}
function createObjects() { <!-- 创建物体函数 -->
// 地面
pos.set( 0, - 0.5, 0 );
quat.set( 0, 0, 0, 1 );
const ground = createParalellepiped( 40, 1, 40, 0, pos, quat, new THREE.MeshPhongMaterial( { color: 0xFFFFFF } ) ); <!-- 创建地面 -->
ground.castShadow = true; <!-- 设置地面投射阴影 -->
ground.receiveShadow = true; <!-- 设置地面接收阴影 -->
textureLoader.load( 'textures/grid.png', function ( texture ) { <!-- 加载地面纹理 -->
texture.colorSpace = THREE.SRGBColorSpace; <!-- 设置纹理色彩空间 -->
texture.wrapS = THREE.RepeatWrapping; <!-- 设置纹理水平重复方式 -->
texture.wrapT = THREE.RepeatWrapping; <!-- 设置纹理垂直重复方式 -->
texture.repeat.set( 40, 40 ); <!-- 设置纹理重复次数 -->
ground.material.map = texture; <!-- 设置地面材质纹理 -->
ground.material.needsUpdate = true; <!-- 更新材质 -->
} );
// 创建软体体积
const volumeMass = 15; <!-- 设置体积质量 -->
const sphereGeometry = new THREE.SphereGeometry( 1.5, 40, 25 ); <!-- 创建球体几何体 -->
sphereGeometry.translate( 5, 5, 0 ); <!-- 设置球体位置 -->
createSoftVolume( sphereGeometry, volumeMass, 250 ); <!-- 创建球体软体体积 -->
const boxGeometry = new THREE.BoxGeometry( 1, 1, 5, 4, 4, 20 ); <!-- 创建盒体几何体 -->
boxGeometry.translate( - 2, 5, 0 ); <!-- 设置盒体位置 -->
createSoftVolume( boxGeometry, volumeMass, 120 ); <!-- 创建盒体软体体积 -->
// 斜坡
pos.set( 3, 1, 0 );
quat.setFromAxisAngle( new THREE.Vector3( 0, 0, 1 ), 30 * Math.PI / 180 );
const obstacle = createParalellepiped( 10, 1, 4, 0, pos, quat, new THREE.MeshPhongMaterial( { color: 0x606060 } ) ); <!-- 创建斜坡 -->
obstacle.castShadow = true; <!-- 设置斜坡投射阴影 -->
obstacle.receiveShadow = true; <!-- 设置斜坡接收阴影 -->
}
function processGeometry( bufGeometry ) {
// 只考虑位置值来合并顶点
const posOnlyBufGeometry = new THREE.BufferGeometry();
posOnlyBufGeometry.setAttribute( 'position', bufGeometry.getAttribute( 'position' ) );
posOnlyBufGeometry.setIndex( bufGeometry.getIndex() );
// 合并顶点以将三角形网格转换为索引三角形
const indexedBufferGeom = BufferGeometryUtils.mergeVertices( posOnlyBufGeometry );
// 创建索引数组,将索引顶点映射到bufGeometry顶点
mapIndices( bufGeometry, indexedBufferGeom );
}
function isEqual( x1, y1, z1, x2, y2, z2 ) {
const delta = 0.000001;
return Math.abs( x2 - x1 ) < delta &&
Math.abs( y2 - y1 ) < delta &&
Math.abs( z2 - z1 ) < delta;
}
function mapIndices( bufGeometry, indexedBufferGeom ) {
// 创建ammoVertices、ammoIndices和ammoIndexAssociation在bufGeometry中
const vertices = bufGeometry.attributes.position.array;
const idxVertices = indexedBufferGeom.attributes.position.array;
const indices = indexedBufferGeom.index.array;
const numIdxVertices = idxVertices.length / 3;
const numVertices = vertices.length / 3;
bufGeometry.ammoVertices = idxVertices;
bufGeometry.ammoIndices = indices;
bufGeometry.ammoIndexAssociation = [];
for ( let i = 0; i < numIdxVertices; i ++ ) {
const association = [];
bufGeometry.ammoIndexAssociation.push( association );
const i3 = i * 3;
for ( let j = 0; j <
numVertices; j ++ ) {
const j3 = j * 3;
if ( isEqual( idxVertices[ i3 ], idxVertices[ i3 + 1 ], idxVertices[ i3 + 2 ],
vertices[ j3 ], vertices[ j3 + 1 ], vertices[ j3 + 2 ] ) ) {
association.push( j3 );
}
}
}
}
function createSoftVolume( bufferGeom, mass, pressure ) {
processGeometry( bufferGeom );
const volume = new THREE.Mesh( bufferGeom, new THREE.MeshPhongMaterial( { color: 0xFFFFFF } ) );
volume.castShadow = true;
volume.receiveShadow = true;
volume.frustumCulled = false;
scene.add( volume );
textureLoader.load( 'textures/colors.png', function ( texture ) {
volume.material.map = texture;
volume.material.needsUpdate = true;
} );
// 体积物理对象
const volumeSoftBody = softBodyHelpers.CreateFromTriMesh(
physicsWorld.getWorldInfo(),
bufferGeom.ammoVertices,
bufferGeom.ammoIndices,
bufferGeom.ammoIndices.length / 3,
true );
const sbConfig = volumeSoftBody.get_m_cfg();
sbConfig.set_viterations( 40 );
sbConfig.set_piterations( 40 );
// 软-软体和软-刚体碰撞
sbConfig.set_collisions( 0x11 );
// 摩擦
sbConfig.set_kDF( 0.1 );
// 阻尼
sbConfig.set_kDP( 0.01 );
// 压力
sbConfig.set_kPR( pressure );
// 刚度
volumeSoftBody.get_m_materials().at( 0 ).set_m_kLST( 0.9 );
volumeSoftBody.get_m_materials().at( 0 ).set_m_kAST( 0.9 );
volumeSoftBody.setTotalMass( mass, false );
Ammo.castObject( volumeSoftBody, Ammo.btCollisionObject ).getCollisionShape().setMargin( margin );
physicsWorld.addSoftBody( volumeSoftBody, 1, - 1 );
volume.userData.physicsBody = volumeSoftBody;
// 禁用去激活
volumeSoftBody.setActivationState( 4 );
softBodies.push( volume );
}
function createParalellepiped( sx, sy, sz, mass, pos, quat, material ) {
const threeObject = new THREE.Mesh( new THREE.BoxGeometry( sx, sy, sz, 1, 1, 1 ), material );
const shape = new Ammo.btBoxShape( new Ammo.btVector3( sx * 0.5, sy * 0.5, sz * 0.5 ) );
shape.setMargin( margin );
createRigidBody( threeObject, shape, mass, pos, quat );
return threeObject;
}
function createRigidBody( threeObject, physicsShape, mass, pos, quat ) {
threeObject.position.copy( pos );
threeObject.quaternion.copy( quat );
const transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin( new Ammo.btVector3( pos.x, pos.y, pos.z ) );
transform.setRotation( new Ammo.btQuaternion( quat.x, quat.y, quat.z, quat.w ) );
const motionState = new Ammo.btDefaultMotionState( transform );
const localInertia = new Ammo.btVector3( 0, 0, 0 );
physicsShape.calculateLocalInertia( mass, localInertia );
const rbInfo = new Ammo.btRigidBodyConstructionInfo( mass, motionState, physicsShape, localInertia );
const body = new Ammo.btRigidBody( rbInfo );
threeObject.userData.physicsBody = body;
scene.add( threeObject );
if ( mass > 0 ) {
rigidBodies.push( threeObject );
// 禁用去激活
body.setActivationState( 4 );
}
physicsWorld.addRigidBody( body );
return body;
}
function initInput() {
window.addEventListener( 'pointerdown', function ( event ) {
if ( ! clickRequest ) {
mouseCoords.set(
( event.clientX / window.innerWidth ) * 2 - 1,
- ( event.clientY / window.innerHeight ) * 2 + 1
);
clickRequest = true;
}
} );
}
function processClick() {
if ( clickRequest ) {
raycaster.setFromCamera( mouseCoords, camera );
// 创建球体
const ballMass = 3;
const ballRadius = 0.4;
const ball = new THREE.Mesh( new THREE.SphereGeometry( ballRadius, 18, 16 ), ballMaterial );
ball.castShadow = true;
ball.receiveShadow = true;
const ballShape = new Ammo.btSphereShape( ballRadius );
ballShape.setMargin( margin );
pos.copy( raycaster.ray.direction );
pos.add( raycaster.ray.origin );
quat.set( 0, 0, 0, 1 );
const ballBody = createRigidBody( ball, ballShape, ballMass, pos, quat );
ballBody.setFriction( 0.5 );
pos.copy( raycaster.ray.direction );
pos.multiplyScalar( 14 );
ballBody.setLinearVelocity( new Ammo.btVector3( pos.x, pos.y, pos.z ) );
clickRequest = false;
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
requestAnimationFrame( animate );
render();
stats.update();
}
function render() {
const deltaTime = clock.getDelta();
updatePhysics( deltaTime );
processClick();
renderer.render( scene, camera );
}
function updatePhysics( deltaTime ) {
// 步进世界
physicsWorld.stepSimulation( deltaTime, 10 );
// 更新软体体积
for ( let i = 0, il = softBodies.length; i < il; i ++ ) {
const volume = softBodies[ i ];
const geometry = volume.geometry;
const softBody = volume.userData.physicsBody;
const volumePositions = geometry.attributes.position.array;
const volumeNormals = geometry.attributes.normal.array;
const association = geometry.ammoIndexAssociation;
const numVerts = association.length;
const nodes = softBody.get_m_nodes();
for ( let j = 0; j < numVerts; j ++ ) {
const node = nodes.at( j );
const nodePos = node.get_m_x();
const x =
nodePos.x();
const y = nodePos.y();
const z = nodePos.z();
const nodeNormal = node.get_m_n();
const normal = association[ j ];
for ( let k = 0, kl = normal.length; k < kl; k ++ ) {
const indexVertex = normal[ k ];
volumePositions[ indexVertex ] = x;
volumePositions[ indexVertex + 1 ] = y;
volumePositions[ indexVertex + 2 ] = z;
volumeNormals[ indexVertex ] = - nodeNormal.x();
volumeNormals[ indexVertex + 1 ] = - nodeNormal.y();
volumeNormals[ indexVertex + 2 ] = - nodeNormal.z();
}
}
geometry.attributes.position.needsUpdate = true;
geometry.attributes.normal.needsUpdate = true;
}
// 更新物体位置
for ( let i = 0, il = rigidBodies.length; i < il; i ++ ) {
const objThree = rigidBodies[ i ];
const objPhys = objThree.userData.physicsBody;
const ms = objPhys.getMotionState();
if ( ms ) {
ms.getWorldTransform( transformAux1 );
const p = transformAux1.getOrigin();
const q = transformAux1.getRotation();
objThree.position.set( p.x(), p.y(), p.z() );
objThree.quaternion.set( q.x(), q.y(), q.z(), q.w() );
}
}
}
</script>
</body>
</html>
本内容来源于小豆包,想要更多内容请跳转小豆包 》