new Ammo.btHeightfieldTerrainShape
创建一个高度场地形的物理形状对象。
构造函数参数:
width
: 高度场的宽度(整数)depth
: 高度场的深度(整数)heightData
: 包含高度数据的内存块的指针heightScale
: 高度数据的缩放比例minHeight
: 高度场的最小高度maxHeight
: 高度场的最大高度upAxis
: 高度场的上轴方向(0表示X轴,1表示Y轴,2表示Z轴)heightDataType
: 高度数据的类型(字符串,例如'PHY_FLOAT'
)flipQuadEdges
: 是否翻转四边形边缘的标志(布尔值)
方法和属性:
setMargin(margin)
: 设置高度场形状的边缘间距setLocalScaling(scale)
: 设置高度场形状的本地缩放getHeight
: 获取指定位置的高度值getMinHeight
: 获取高度场的最小高度getMaxHeight
: 获取高度场的最大高度upAxis
: 高度场的上轴方向heightDataType
: 高度数据的类型flipQuadEdges
: 是否翻转四边形边缘的标志
注意事项:
heightData
应该是一个指向包含高度数据的内存块的指针,格式化为指定的高度数据类型。- 在使用之前,应确保使用了Ammo.js的内存分配器来为
heightData
分配内存,并在不再需要时正确释放内存。
demo 源码
<!DOCTYPE html>
<html lang="en">
<head>
<title>Ammo.js terrain heightfield demo</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="container"></div>
<div id="info">Ammo.js physics terrain heightfield demo</div>
<!-- 引入Ammo.js -->
<script src="jsm/libs/ammo.wasm.js"></script>
<!-- 导入映射关系 -->
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<!-- 使用ES6模块 -->
<script type="module">
// 导入Three.js库
import * as THREE from 'three';
// 导入性能统计模块
import Stats from 'three/addons/libs/stats.module.js';
// 导入轨道控制器
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 地形参数
const terrainWidthExtents = 100;
const terrainDepthExtents = 100;
const terrainWidth = 128;
const terrainDepth = 128;
const terrainHalfWidth = terrainWidth / 2;
const terrainHalfDepth = terrainDepth / 2;
const terrainMaxHeight = 8;
const terrainMinHeight = - 2;
let container, stats;
let camera, scene, renderer;
let terrainMesh;
const clock = new THREE.Clock();
// 物理变量
let collisionConfiguration;
let dispatcher;
let broadphase;
let solver;
let physicsWorld;
const dynamicObjects = [];
let transformAux1;
let heightData = null;
let ammoHeightData = null;
let time = 0;
const objectTimePeriod = 3;
let timeNextSpawn = time + objectTimePeriod;
const maxNumObjects = 30;
// 加载Ammo.js
Ammo().then( function ( AmmoLib ) {
Ammo = AmmoLib;
init();
animate();
} );
// 初始化函数
function init() {
// 生成地形高度数据
heightData = generateHeight( terrainWidth, terrainDepth, terrainMinHeight, terrainMaxHeight );
// 初始化图形界面
initGraphics();
// 初始化物理世界
initPhysics();
}
// 初始化图形界面
function initGraphics() {
// 获取容器元素
container = document.getElementById( 'container' );
// 创建渲染器
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;
container.appendChild( renderer.domElement );
// 创建性能统计器
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
container.appendChild( stats.domElement );
// 创建相机
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.2, 2000 );
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xbfd1e5 );
// 设置相机位置
camera.position.y = heightData[ terrainHalfWidth + terrainHalfDepth * terrainWidth ] * ( terrainMaxHeight - terrainMinHeight ) + 5;
camera.position.z = terrainDepthExtents / 2;
camera.lookAt( 0, 0, 0 );
// 创建轨道控制器
const controls = new OrbitControls( camera, renderer.domElement );
controls.enableZoom = false;
// 创建地形几何体
const geometry = new THREE.PlaneGeometry( terrainWidthExtents, terrainDepthExtents, terrainWidth - 1, terrainDepth - 1 );
geometry.rotateX( - Math.PI / 2 );
// 设置顶点高度
const vertices = geometry.attributes.position.array;
for ( let i = 0, j = 0, l = vertices.length; i < l; i ++, j += 3 ) {
vertices[ j + 1 ] = heightData[ i ];
}
geometry.computeVertexNormals();
// 创建地形网格
const groundMaterial = new THREE.MeshPhongMaterial( { color: 0xC7C7C7 } );
terrainMesh = new THREE.Mesh( geometry, groundMaterial );
terrainMesh.receiveShadow = true;
terrainMesh.castShadow = true;
scene.add( terrainMesh );
// 加载纹理
const textureLoader = new THREE.TextureLoader();
textureLoader.load( 'textures/grid.png', function ( texture ) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set( terrainWidth - 1, terrainDepth - 1 );
groundMaterial.map = texture;
groundMaterial.needsUpdate = true;
} );
// 添加环境光
const ambientLight = new THREE.AmbientLight( 0xbbbbbb );
scene.add( ambientLight );
// 添加平行光
const light = new THREE.DirectionalLight( 0xffffff, 3 );
light.position.set( 100, 100, 50 );
light.castShadow = true;
const dLight = 200;
const sLight = dLight * 0.25;
light.shadow.camera.left = - sLight;
light.shadow.camera.right = sLight;
light.shadow.camera.top = sLight;
light.shadow.camera.bottom = - sLight;
light.shadow.camera.near = dLight / 30;
light.shadow.camera.far = dLight;
light.shadow.mapSize.x = 1024 * 2;
light.shadow.mapSize.y = 1024 * 2;
scene.add( light );
// 窗口大小改变事件监听器
window.addEventListener( 'resize', onWindowResize );
}
// 窗口大小改变处理函数
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
// 初始化物理世界
function initPhysics() {
// 物理配置
collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration );
broadphase = new Ammo
.btDbvtBroadphase();
solver = new Ammo.btSequentialImpulseConstraintSolver();
physicsWorld = new Ammo.btDiscreteDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration );
physicsWorld.setGravity( new Ammo.btVector3( 0, - 6, 0 ) );
// 创建地形物体
const groundShape = createTerrainShape();
const groundTransform = new Ammo.btTransform();
groundTransform.setIdentity();
groundTransform.setOrigin( new Ammo.btVector3( 0, ( terrainMaxHeight + terrainMinHeight ) / 2, 0 ) );
const groundMass = 0;
const groundLocalInertia = new Ammo.btVector3( 0, 0, 0 );
const groundMotionState = new Ammo.btDefaultMotionState( groundTransform );
const groundBody = new Ammo.btRigidBody( new Ammo.btRigidBodyConstructionInfo( groundMass, groundMotionState, groundShape, groundLocalInertia ) );
physicsWorld.addRigidBody( groundBody );
transformAux1 = new Ammo.btTransform();
}
// 生成高度数据
function generateHeight( width, depth, minHeight, maxHeight ) {
const size = width * depth;
const data = new Float32Array( size );
const hRange = maxHeight - minHeight;
const w2 = width / 2;
const d2 = depth / 2;
const phaseMult = 12;
let p = 0;
for ( let j = 0; j < depth; j ++ ) {
for ( let i = 0; i < width; i ++ ) {
const radius = Math.sqrt(
Math.pow( ( i - w2 ) / w2, 2.0 ) +
Math.pow( ( j - d2 ) / d2, 2.0 ) );
const height = ( Math.sin( radius * phaseMult ) + 1 ) * 0.5 * hRange + minHeight;
data[ p ] = height;
p ++;
}
}
return data;
}
// 创建地形形状
function createTerrainShape() {
const heightScale = 1;
const upAxis = 1;
const hdt = 'PHY_FLOAT';
const flipQuadEdges = false;
ammoHeightData = Ammo._malloc( 4 * terrainWidth * terrainDepth );
let p = 0;
let p2 = 0;
for ( let j = 0; j < terrainDepth; j ++ ) {
for ( let i = 0; i < terrainWidth; i ++ ) {
Ammo.HEAPF32[ ammoHeightData + p2 >> 2 ] = heightData[ p ];
p ++;
p2 += 4;
}
}
const heightFieldShape = new Ammo.btHeightfieldTerrainShape(
terrainWidth,
terrainDepth,
ammoHeightData,
heightScale,
terrainMinHeight,
terrainMaxHeight,
upAxis,
hdt,
flipQuadEdges
);
const scaleX = terrainWidthExtents / ( terrainWidth - 1 );
const scaleZ = terrainDepthExtents / ( terrainDepth - 1 );
heightFieldShape.setLocalScaling( new Ammo.btVector3( scaleX, 1, scaleZ ) );
heightFieldShape.setMargin( 0.05 );
return heightFieldShape;
}
function generateObject() {
// 生成随机的物体类型(1到4)
const numTypes = 4;
const objectType = Math.ceil( Math.random() * numTypes );
let threeObject = null;
let shape = null;
const objectSize = 3;
const margin = 0.05;
let radius, height;
switch ( objectType ) {
// 生成球体
case 1:
radius = 1 + Math.random() * objectSize;
threeObject = new THREE.Mesh( new THREE.SphereGeometry( radius, 20, 20 ), createObjectMaterial() );
shape = new Ammo.btSphereShape( radius );
shape.setMargin( margin );
break;
// 生成盒子
case 2:
const sx = 1 + Math.random() * objectSize;
const sy = 1 + Math.random() * objectSize;
const sz = 1 + Math.random() * objectSize;
threeObject = new THREE.Mesh( new THREE.BoxGeometry( sx, sy, sz, 1, 1, 1 ), createObjectMaterial() );
shape = new Ammo.btBoxShape( new Ammo.btVector3( sx * 0.5, sy * 0.5, sz * 0.5 ) );
shape.setMargin( margin );
break;
// 生成圆柱体
case 3:
radius = 1 + Math.random() * objectSize;
height = 1 + Math.random() * objectSize;
threeObject = new THREE.Mesh( new THREE.CylinderGeometry( radius, radius, height, 20, 1 ), createObjectMaterial() );
shape = new Ammo.btCylinderShape( new Ammo.btVector3( radius, height * 0.5, radius ) );
shape.setMargin( margin );
break;
// 默认生成圆锥体
default:
radius = 1 + Math.random() * objectSize;
height = 2 + Math.random() * objectSize;
threeObject = new THREE.Mesh( new THREE.ConeGeometry( radius, height, 20, 2 ), createObjectMaterial() );
shape = new Ammo.btConeShape( radius, height );
break;
}
// 设置物体的位置
threeObject.position.set( ( Math.random() - 0.5 ) * terrainWidth * 0.6, terrainMaxHeight + objectSize + 2, ( Math.random() - 0.5 ) * terrainDepth * 0.6 );
// 计算物体的质量和惯性
const mass = objectSize * 5;
const localInertia = new Ammo.btVector3( 0, 0, 0 );
shape.calculateLocalInertia( mass, localInertia );
// 创建物体的刚体
const transform = new Ammo.btTransform();
transform.setIdentity();
const pos = threeObject.position;
transform.setOrigin( new Ammo.btVector3( pos.x, pos.y, pos.z ) );
const motionState = new Ammo.btDefaultMotionState( transform );
const rbInfo = new Ammo.btRigidBodyConstructionInfo( mass, motionState, shape, localInertia );
const body = new Ammo.btRigidBody( rbInfo );
// 将刚体与物体关联
threeObject.userData.physicsBody = body;
threeObject.receiveShadow = true;
threeObject.castShadow = true;
// 将物体添加到场景中并保存到dynamicObjects数组中
scene.add( threeObject );
dynamicObjects.push( threeObject );
physicsWorld.addRigidBody( body );
}
// 创建物体材质的函数
function createObjectMaterial() {
const c = Math.floor( Math.random() * ( 1 << 24 ) );
return new THREE.MeshPhongMaterial( { color: c } );
}
// 动画函数
function animate() {
requestAnimationFrame( animate );
render();
stats.update();
}
// 渲染函数
function render() {
const deltaTime = clock.getDelta();
// 每隔一段时间生成一个物体
if ( dynamicObjects.length < maxNumObjects && time > timeNextSpawn ) {
generateObject();
timeNextSpawn = time + objectTimePeriod;
}
// 更新物理世界
updatePhysics( deltaTime );
// 渲染场景
renderer.render( scene, camera );
time += deltaTime;
}
// 更新物理世界函数
function updatePhysics( deltaTime ) {
physicsWorld.stepSimulation( deltaTime, 10 );
// 更新物体位置和姿态
for ( let i = 0, il = dynamicObjects.length; i < il; i ++ ) {
const objThree = dynamicObjects[ 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>