You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
449 lines
12 KiB
449 lines
12 KiB
2 years ago
|
<!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>
|
||
|
|
||
|
<script src="js/libs/ammo.wasm.js"></script>
|
||
|
|
||
|
<!-- Import maps polyfill -->
|
||
|
<!-- Remove this when import maps will be widely supported -->
|
||
|
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
|
||
|
|
||
|
<script type="importmap">
|
||
|
{
|
||
|
"imports": {
|
||
|
"three": "../build/three.module.js",
|
||
|
"three/addons/": "./jsm/"
|
||
|
}
|
||
|
}
|
||
|
</script>
|
||
|
|
||
|
<script type="module">
|
||
|
|
||
|
import * as THREE from 'three';
|
||
|
|
||
|
import Stats from 'three/addons/libs/stats.module.js';
|
||
|
|
||
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||
|
|
||
|
// Heightfield parameters
|
||
|
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;
|
||
|
|
||
|
// Graphics variables
|
||
|
let container, stats;
|
||
|
let camera, scene, renderer;
|
||
|
let terrainMesh;
|
||
|
const clock = new THREE.Clock();
|
||
|
|
||
|
// Physics variables
|
||
|
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().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();
|
||
|
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 ) {
|
||
|
|
||
|
// j + 1 because it is the y component that we modify
|
||
|
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 light = new THREE.DirectionalLight( 0xffffff, 1 );
|
||
|
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() {
|
||
|
|
||
|
// Physics configuration
|
||
|
|
||
|
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 ) );
|
||
|
|
||
|
// Create the terrain body
|
||
|
|
||
|
const groundShape = createTerrainShape();
|
||
|
const groundTransform = new Ammo.btTransform();
|
||
|
groundTransform.setIdentity();
|
||
|
// Shifts the terrain, since bullet re-centers it on its bounding box.
|
||
|
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 ) {
|
||
|
|
||
|
// Generates the height data (a sinus wave)
|
||
|
|
||
|
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() {
|
||
|
|
||
|
// This parameter is not really used, since we are using PHY_FLOAT height data type and hence it is ignored
|
||
|
const heightScale = 1;
|
||
|
|
||
|
// Up axis = 0 for X, 1 for Y, 2 for Z. Normally 1 = Y is used.
|
||
|
const upAxis = 1;
|
||
|
|
||
|
// hdt, height data type. "PHY_FLOAT" is used. Possible values are "PHY_FLOAT", "PHY_UCHAR", "PHY_SHORT"
|
||
|
const hdt = 'PHY_FLOAT';
|
||
|
|
||
|
// Set this to your needs (inverts the triangles)
|
||
|
const flipQuadEdges = false;
|
||
|
|
||
|
// Creates height data buffer in Ammo heap
|
||
|
ammoHeightData = Ammo._malloc( 4 * terrainWidth * terrainDepth );
|
||
|
|
||
|
// Copy the javascript height data array to the Ammo one.
|
||
|
let p = 0;
|
||
|
let p2 = 0;
|
||
|
|
||
|
for ( let j = 0; j < terrainDepth; j ++ ) {
|
||
|
|
||
|
for ( let i = 0; i < terrainWidth; i ++ ) {
|
||
|
|
||
|
// write 32-bit float data to memory
|
||
|
Ammo.HEAPF32[ ammoHeightData + p2 >> 2 ] = heightData[ p ];
|
||
|
|
||
|
p ++;
|
||
|
|
||
|
// 4 bytes/float
|
||
|
p2 += 4;
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
// Creates the heightfield physics shape
|
||
|
const heightFieldShape = new Ammo.btHeightfieldTerrainShape(
|
||
|
terrainWidth,
|
||
|
terrainDepth,
|
||
|
ammoHeightData,
|
||
|
heightScale,
|
||
|
terrainMinHeight,
|
||
|
terrainMaxHeight,
|
||
|
upAxis,
|
||
|
hdt,
|
||
|
flipQuadEdges
|
||
|
);
|
||
|
|
||
|
// Set horizontal scale
|
||
|
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() {
|
||
|
|
||
|
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:
|
||
|
// Sphere
|
||
|
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:
|
||
|
// Box
|
||
|
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:
|
||
|
// Cylinder
|
||
|
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:
|
||
|
// Cone
|
||
|
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;
|
||
|
|
||
|
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 );
|
||
|
|
||
|
// Update objects
|
||
|
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>
|