Browse Source

Added additional Rapier physics examples (#30818)

* Added new Rapier physics examples

* Delete attribute when using Rapier debugger

* Added screenshots

* Restored jsDocs to RapierPhysics.js

* Updated files.json

* added exception

* Updates for Michael

* Refactored debugger as RapierHelper

* Removed unused imports from RapierPhysics.js

* Update RapierPhysics.js

* Update RapierPhysics.js

Fix code style.

* Refactored physics_rapier_joints.html to not use getBody.

* Removed getBody from RapierPhysics.js

* Update RapierPhysics.js

* Update RapierPhysics.js

* Refactored access to the RigidBody

* Update physics_rapier_vehicle_controller.html

---------

Co-authored-by: Michael Herzog <michael.herzog@human-interactive.org>
Nik Lever 10 months ago
parent
commit
7f74cfa8bc

+ 5 - 1
examples/files.json

@@ -502,7 +502,11 @@
 		"physics_ammo_terrain",
 		"physics_ammo_volume",
 		"physics_jolt_instancing",
-		"physics_rapier_instancing"
+		"physics_rapier_basic",
+		"physics_rapier_instancing",
+		"physics_rapier_joints",
+		"physics_rapier_character_controller",
+		"physics_rapier_vehicle_controller"
 	],
 	"misc": [
 		"misc_animation_groups",

+ 58 - 0
examples/jsm/helpers/RapierHelper.js

@@ -0,0 +1,58 @@
+import { LineSegments, LineBasicMaterial, BufferAttribute } from 'three';
+/**
+ * This class displays all Rapier Colliders in outline.
+ *
+ * @augments LineSegments
+ */
+class RapierHelper extends LineSegments {
+
+	/**
+	 * Constructs a new Rapier debug helper.
+	 *
+	 * @param {RAPIER.world} world - The Rapier world to visualize.
+	 */
+	constructor( world ) {
+
+		super();
+
+		/**
+		 * The Rapier world to visualize.
+		 *
+		 * @type {RAPIER.world}
+		 */
+		this.world = world;
+
+		this.material = new LineBasicMaterial( { vertexColors: true } );
+		this.frustumCulled = false;
+
+	}
+
+	/**
+	 * Call this in the render loop to update the outlines.
+	 */
+	update() {
+
+		const { vertices, colors } = this.world.debugRender();
+
+		this.geometry.deleteAttribute( 'position' );
+		this.geometry.deleteAttribute( 'color' );
+
+		this.geometry.setAttribute( 'position', new BufferAttribute( vertices, 3 ) );
+		this.geometry.setAttribute( 'color', new BufferAttribute( colors, 4 ) );
+
+	}
+
+	/**
+	 * Frees the GPU-related resources allocated by this instance. Call this
+	 * method whenever this instance is no longer used in your app.
+	 */
+	dispose() {
+
+		this.geometry.dispose();
+		this.material.dispose();
+
+	}
+
+}
+
+export { RapierHelper };

+ 20 - 0
examples/jsm/physics/RapierPhysics.js

@@ -28,6 +28,20 @@ function getShape( geometry ) {
 		const radius = parameters.radius !== undefined ? parameters.radius : 1;
 		return RAPIER.ColliderDesc.ball( radius );
 
+	} else if ( geometry.type === 'CylinderGeometry' ) {
+
+		const radius = parameters.radiusBottom !== undefined ? parameters.radiusBottom : 0.5;
+		const length = parameters.length !== undefined ? parameters.length : 0.5;
+
+		return RAPIER.ColliderDesc.cylinder( length / 2, radius );
+
+	} else if ( geometry.type === 'CapsuleGeometry' ) {
+
+		const radius = parameters.radius !== undefined ? parameters.radius : 0.5;
+		const length = parameters.length !== undefined ? parameters.length : 0.5;
+
+		return RAPIER.ColliderDesc.capsule( length / 2, radius );
+
 	} else if ( geometry.type === 'BufferGeometry' ) {
 
 		const vertices = [];
@@ -121,6 +135,10 @@ async function RapierPhysics() {
 			? createInstancedBody( mesh, mass, shape )
 			: createBody( mesh.position, mesh.quaternion, mass, shape );
 
+		if ( ! mesh.userData.physics ) mesh.userData.physics = {};
+
+		mesh.userData.physics.body = body;
+
 		if ( mass > 0 ) {
 
 			meshes.push( mesh );
@@ -242,6 +260,8 @@ async function RapierPhysics() {
 	setInterval( step, 1000 / frameRate );
 
 	return {
+		RAPIER,
+		world,
 		/**
 		 * Adds the given scene to this physics simulation. Only meshes with a
 		 * `physics` object in their {@link Object3D#userData} field will be honored.

+ 172 - 0
examples/physics_rapier_basic.html

@@ -0,0 +1,172 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js physics - rapier3d basic</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+		<link type="text/css" rel="stylesheet" href="main.css">
+		<style>
+			body {
+				color: #333;
+			}
+		</style>
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> physics - <a href="https://github.com/dimforge/rapier.js" target="_blank">rapier</a> basic
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { RapierPhysics } from 'three/addons/physics/RapierPhysics.js';
+			import { RapierHelper } from 'three/addons/helpers/RapierHelper.js';
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			let camera, scene, renderer, stats;
+			let physics, physicsHelper;
+
+			init();
+
+			async function init() {
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0xbfd1e5 );
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 0, 3, 10 );
+
+				const ambient = new THREE.HemisphereLight( 0x555555, 0xFFFFFF );
+
+				scene.add( ambient );
+
+				const light = new THREE.DirectionalLight( 0xffffff, 4 );
+
+				light.position.set( 0, 12.5, 12.5 );
+				light.castShadow = true;
+				light.shadow.radius = 3;
+				light.shadow.blurSamples = 8;
+				light.shadow.mapSize.width = 1024;
+				light.shadow.mapSize.height = 1024;
+
+				const size = 10;
+				light.shadow.camera.left = - size;
+				light.shadow.camera.bottom = - size;
+				light.shadow.camera.right = size;
+				light.shadow.camera.top = size;
+				light.shadow.camera.near = 1;
+				light.shadow.camera.far = 50;
+
+				scene.add( light );
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.shadowMap.enabled = true;
+				document.body.appendChild( renderer.domElement );
+				renderer.setAnimationLoop( animate );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target = new THREE.Vector3( 0, 2, 0 );
+				controls.update();
+
+				const geometry = new THREE.BoxGeometry( 10, 0.5, 10 );
+				const material = new THREE.MeshStandardMaterial( { color: 0xFFFFFF } );
+
+				const floor = new THREE.Mesh( geometry, material );
+				floor.receiveShadow = true;
+
+				floor.position.y = - 0.25;
+				floor.userData.physics = { mass: 0 };
+
+				scene.add( floor );
+
+				new THREE.TextureLoader().load( 'textures/grid.png', function ( texture ) {
+
+					texture.wrapS = THREE.RepeatWrapping;
+					texture.wrapT = THREE.RepeatWrapping;
+					texture.repeat.set( 20, 20 );
+					floor.material.map = texture;
+					floor.material.needsUpdate = true;
+
+				} );
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				initPhysics();
+
+				onWindowResize();
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+			}
+
+			async function initPhysics() {
+
+				//Initialize physics engine using the script in the jsm/physics folder
+				physics = await RapierPhysics();
+
+				physics.addScene( scene );
+
+				addBody( );
+
+				//Optionally display collider outlines
+				physicsHelper = new RapierHelper( physics.world );
+				scene.add( physicsHelper );
+
+				setInterval( addBody, 1000 );
+
+			}
+
+			function addBody( ) {
+
+				const geometry = ( Math.random() > 0.5 ) ? new THREE.SphereGeometry( 0.5 ) : new THREE.BoxGeometry( 1, 1, 1 );
+				const material = new THREE.MeshStandardMaterial( { color: Math.floor( Math.random() * 0xFFFFFF ) } );
+
+				const mesh = new THREE.Mesh( geometry, material );
+				mesh.castShadow = true;
+
+				mesh.position.set( Math.random() * 2 - 1, Math.random() * 3 + 6, Math.random() * 2 - 1 );
+
+				scene.add( mesh );
+
+				//parameter 2 - mass, parameter 3 - restitution ( how bouncy )
+				physics.addMesh( mesh, 1, 0.5 );
+
+			}
+
+			function onWindowResize( ) {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+
+			function animate() {
+
+				if ( physicsHelper ) physicsHelper.update();
+
+				renderer.render( scene, camera );
+
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 243 - 0
examples/physics_rapier_character_controller.html

@@ -0,0 +1,243 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js physics - rapier3d character controller</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+		<link type="text/css" rel="stylesheet" href="main.css">
+		<style>
+			body {
+				color: #333;
+			}
+		</style>
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> physics - <a href="https://github.com/dimforge/rapier.js" target="_blank">rapier</a> character controller
+			<p>WASD or Arrow keys to move</p>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { RapierPhysics } from 'three/addons/physics/RapierPhysics.js';
+			import { RapierHelper } from 'three/addons/helpers/RapierHelper.js';
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			let camera, scene, renderer, stats;
+			let physics, characterController, physicsHelper;
+			let player, movement;
+
+			init();
+
+			async function init() {
+
+				scene = new THREE.Scene();
+
+				scene.background = new THREE.Color( 0xbfd1e5 );
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 2, 5, 15 );
+
+				const ambient = new THREE.HemisphereLight( 0x555555, 0xFFFFFF );
+
+				scene.add( ambient );
+
+				const light = new THREE.DirectionalLight( 0xffffff, 3 );
+
+				light.position.set( 0, 12.5, 12.5 );
+				light.castShadow = true;
+				light.shadow.radius = 3;
+				light.shadow.blurSamples = 8;
+				light.shadow.mapSize.width = 1024;
+				light.shadow.mapSize.height = 1024;
+
+				const size = 10;
+				light.shadow.camera.left = - size;
+				light.shadow.camera.bottom = - size;
+				light.shadow.camera.right = size;
+				light.shadow.camera.top = size;
+				light.shadow.camera.near = 1;
+				light.shadow.camera.far = 50;
+
+				scene.add( light );
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.shadowMap.enabled = true;
+				document.body.appendChild( renderer.domElement );
+				renderer.setAnimationLoop( animate );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target = new THREE.Vector3( 0, 2, 0 );
+				controls.update();
+
+				const geometry = new THREE.BoxGeometry( 20, 0.5, 20 );
+				const material = new THREE.MeshStandardMaterial( { color: 0xFFFFFF } );
+
+				const ground = new THREE.Mesh( geometry, material );
+				ground.receiveShadow = true;
+
+				ground.position.y = - 0.25;
+				ground.userData.physics = { mass: 0 };
+
+				scene.add( ground );
+
+				new THREE.TextureLoader().load( 'textures/grid.png', function ( texture ) {
+
+					texture.wrapS = THREE.RepeatWrapping;
+					texture.wrapT = THREE.RepeatWrapping;
+					texture.repeat.set( 20, 20 );
+					ground.material.map = texture;
+					ground.material.needsUpdate = true;
+
+				} );
+
+				for ( let i = 0; i < 10; i ++ ) addBody( Math.random() > 0.7 );
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				initPhysics();
+
+				onWindowResize();
+
+				// Movement input
+				movement = { forward: 0, right: 0 };
+
+				window.addEventListener( 'keydown', ( event ) => {
+
+					if ( event.key === 'w' || event.key === 'ArrowUp' ) movement.forward = 1;
+					if ( event.key === 's' || event.key === 'ArrowDown' ) movement.forward = - 1;
+					if ( event.key === 'a' || event.key === 'ArrowLeft' ) movement.right = - 1;
+					if ( event.key === 'd' || event.key === 'ArrowRight' ) movement.right = 1;
+			
+				} );
+
+				window.addEventListener( 'keyup', ( event ) => {
+			
+					if ( event.key === 'w' || event.key === 's' || event.key === 'ArrowUp' || event.key === 'ArrowDown' ) movement.forward = 0;
+					if ( event.key === 'a' || event.key === 'd' || event.key === 'ArrowLeft' || event.key === 'ArrowRight' ) movement.right = 0;
+			
+				} );
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+			}
+
+			function random( min, max ) {
+
+				return Math.random() * ( max - min ) + min;
+
+			}
+
+			async function initPhysics() {
+
+				//Initialize physics engine using the script in the jsm/physics folder
+				physics = await RapierPhysics();
+
+				//Optionally display collider outlines
+				physicsHelper = new RapierHelper( physics.world );
+				scene.add( physicsHelper );
+
+				physics.addScene( scene );
+
+				addCharacterController( );
+
+			}
+
+			function addCharacterController() {
+
+				// Character Capsule
+				const geometry = new THREE.CapsuleGeometry( 0.3, 1, 8, 8 );
+				const material = new THREE.MeshStandardMaterial( { color: 0x0000ff } );
+				player = new THREE.Mesh( geometry, material );
+				player.castShadow = true;
+				player.position.set( 0, 0.8, 0 );
+				scene.add( player );
+
+				// Rapier Character Controller
+				characterController = physics.world.createCharacterController( 0.01 );
+				characterController.setApplyImpulsesToDynamicBodies( true );
+				characterController.setCharacterMass( 3 );
+				const colliderDesc = physics.RAPIER.ColliderDesc.capsule( 0.5, 0.3 ).setTranslation( 0, 0.8, 0 );
+				player.userData.collider = physics.world.createCollider( colliderDesc );
+
+			}
+
+			function addBody( fixed = true ) {
+			
+				const geometry = ( fixed ) ? new THREE.BoxGeometry( 1, 1, 1 ) : new THREE.SphereGeometry( 0.25 );
+				const material = new THREE.MeshStandardMaterial( { color: fixed ? 0xFF0000 : 0x00FF00 } );
+
+				const mesh = new THREE.Mesh( geometry, material );
+				mesh.castShadow = true;
+
+				mesh.position.set( random( - 10, 10 ), 0.5, random( - 10, 10 ) );
+
+				mesh.userData.physics = { mass: fixed ? 0 : 0.5, restitution: fixed ? 0 : 0.3 };
+
+				scene.add( mesh );
+
+			}
+
+			function onWindowResize( ) {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+
+			function animate() {
+
+				if ( physicsHelper ) physicsHelper.update();
+
+				renderer.render( scene, camera );
+
+				if ( physics && characterController ) {
+
+					const deltaTime = 1 / 60;
+
+					// Character movement
+					const speed = 2.5 * deltaTime;
+					const moveVector = new physics.RAPIER.Vector3( movement.right * speed, 0, - movement.forward * speed );
+
+					characterController.computeColliderMovement( player.userData.collider, moveVector );
+
+					// Read the result.
+					const translation = characterController.computedMovement();
+					const position = player.userData.collider.translation();
+
+					position.x += translation.x;
+					position.y += translation.y;
+					position.z += translation.z;
+
+					player.userData.collider.setTranslation( position );
+
+					// Sync Three.js mesh with Rapier collider
+					player.position.set( position.x, position.y, position.z );
+
+				}
+
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 175 - 0
examples/physics_rapier_joints.html

@@ -0,0 +1,175 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js physics - rapier3d joints</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+		<link type="text/css" rel="stylesheet" href="main.css">
+		<style>
+			body {
+				color: #333;
+			}
+		</style>
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> physics - <a href="https://github.com/dimforge/rapier.js" target="_blank">rapier</a> joints
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { RapierPhysics } from 'three/addons/physics/RapierPhysics.js';
+			import { RapierHelper } from 'three/addons/helpers/RapierHelper.js';
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			let camera, scene, renderer, stats;
+			let physics, pivot, physicsHelper;
+
+			init();
+
+			async function init() {
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0xbfd1e5 );
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 0, 3, 10 );
+
+				const ambient = new THREE.HemisphereLight( 0x555555, 0xFFFFFF );
+
+				scene.add( ambient );
+
+				const light = new THREE.DirectionalLight( 0xffffff, 4 );
+
+				light.position.set( 0, 12.5, 12.5 );
+				light.castShadow = true;
+				light.shadow.radius = 3;
+				light.shadow.blurSamples = 8;
+				light.shadow.mapSize.width = 1024;
+				light.shadow.mapSize.height = 1024;
+
+				const size = 10;
+				light.shadow.camera.left = - size;
+				light.shadow.camera.bottom = - size;
+				light.shadow.camera.right = size;
+				light.shadow.camera.top = size;
+				light.shadow.camera.near = 1;
+				light.shadow.camera.far = 50;
+
+				scene.add( light );
+			
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.shadowMap.enabled = true;
+				document.body.appendChild( renderer.domElement );
+				renderer.setAnimationLoop( animate );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target = new THREE.Vector3( 0, 2, 0 );
+				controls.update();
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				//Create pivot point
+				const geometry = new THREE.SphereGeometry( 0.5 );
+				const material = new THREE.MeshStandardMaterial( { color: 0xFF0000 } );
+			
+				pivot = new THREE.Mesh( geometry, material );
+
+				pivot.position.y = 6;
+				pivot.userData.physics = { mass: 0 };
+
+				scene.add( pivot );
+			
+				initPhysics();
+			
+				onWindowResize();
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+			}
+
+			async function initPhysics() {
+
+				//Initialize physics engine using the script in the jsm/physics folder
+				physics = await RapierPhysics();
+
+				physics.addScene( scene );
+
+				//Optionally display collider outlines
+				physicsHelper = new RapierHelper( physics.world );
+				scene.add( physicsHelper );
+
+				const link1 = addLink( pivot, 0 );
+				const link2 = addLink( link1, 2 );
+				addLink( link2, 4 );
+
+			}
+
+			//link - the mesh that the new link will be attached to
+			//x    - used to position the new link
+			function addLink( link, x ) {
+
+				const geometry = new THREE.CapsuleGeometry( 0.25, 1.8 );
+				const material = new THREE.MeshStandardMaterial( { color: 0xCCCC00 } );
+			
+				const mesh = new THREE.Mesh( geometry, material );
+				mesh.rotateZ( Math.PI * 0.5 );
+
+				mesh.position.set( x + 0.9, 5.8, 0 );
+
+				scene.add( mesh );
+
+				physics.addMesh( mesh, 1, 0.5 );
+
+				const jointParams = physics.RAPIER.JointData.spherical(
+					( link == pivot ) ? new physics.RAPIER.Vector3( 0, - 0.5, 0 ) : new physics.RAPIER.Vector3( 0, - 1.15, 0 ), // Joint position in world space
+					new physics.RAPIER.Vector3( 0, 1.15, 0 ) // Corresponding attachment on sphere
+				);
+				const body1 = link.userData.physics.body;
+				const body2 = mesh.userData.physics.body;
+				body2.setAngularDamping( 10.0 );
+
+				physics.world.createImpulseJoint( jointParams, body1, body2, true );
+
+				return mesh;
+
+			}
+
+			function onWindowResize( ) {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+
+			function animate() {
+
+				if ( physicsHelper ) physicsHelper.update();
+
+				renderer.render( scene, camera );
+
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

+ 377 - 0
examples/physics_rapier_vehicle_controller.html

@@ -0,0 +1,377 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js physics - rapier3d vehicle controller</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+		<link type="text/css" rel="stylesheet" href="main.css">
+		<style>
+			body {
+				color: #333;
+			}
+		</style>
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> physics - <a href="https://github.com/dimforge/rapier.js" target="_blank">rapier</a> vehicle controller
+			<p>WASD or Arrow keys to move</p>
+			<p>Space to brake</p>
+   			<p>R to reset</p>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { RapierPhysics } from 'three/addons/physics/RapierPhysics.js';
+			import { RapierHelper } from 'three/addons/helpers/RapierHelper.js';
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			let camera, scene, renderer, stats;
+			let physics, physicsHelper, controls;
+			let car, chassis, wheels, movement, vehicleController;
+
+			init();
+
+			async function init() {
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0xbfd1e5 );
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 0, 4, 10 );
+
+				const ambient = new THREE.HemisphereLight( 0x555555, 0xFFFFFF );
+
+				scene.add( ambient );
+
+				const light = new THREE.DirectionalLight( 0xffffff, 4 );
+
+				light.position.set( 0, 12.5, 12.5 );
+				light.castShadow = true;
+				light.shadow.radius = 3;
+				light.shadow.blurSamples = 8;
+				light.shadow.mapSize.width = 2048;
+				light.shadow.mapSize.height = 2048;
+
+				const size = 40;
+				light.shadow.camera.left = - size;
+				light.shadow.camera.bottom = - size;
+				light.shadow.camera.right = size;
+				light.shadow.camera.top = size;
+				light.shadow.camera.near = 1;
+				light.shadow.camera.far = 50;
+
+				scene.add( light );
+			
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.shadowMap.enabled = true;
+				document.body.appendChild( renderer.domElement );
+				renderer.setAnimationLoop( animate );
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.target = new THREE.Vector3( 0, 2, 0 );
+				controls.update();
+
+				const geometry = new THREE.BoxGeometry( 100, 0.5, 100 );
+				const material = new THREE.MeshStandardMaterial( { color: 0xFFFFFF } );
+			
+				const ground = new THREE.Mesh( geometry, material );
+				ground.receiveShadow = true;
+
+				ground.position.set( 0, - 0.25, - 20 );
+				ground.userData.physics = { mass: 0 };
+
+  				scene.add( ground );
+
+				new THREE.TextureLoader().load( 'textures/grid.png', function ( texture ) {
+
+					texture.wrapS = THREE.RepeatWrapping;
+					texture.wrapT = THREE.RepeatWrapping;
+					texture.repeat.set( 80, 80 );
+					ground.material.map = texture;
+					ground.material.needsUpdate = true;
+
+				} );
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+			
+				initPhysics();
+			
+				onWindowResize();
+
+				// Movement input
+				movement = {
+					forward: 0,
+					right: 0,
+					brake: 0,
+					reset: false,
+					accelerateForce: { value: 0, min: - 30, max: 30, step: 1 },
+					brakeForce: { value: 0, min: 0, max: 1, step: 0.05 }
+				};
+
+				window.addEventListener( 'keydown', ( event ) => {
+
+					//console.log( event.key );
+					if ( event.key === 'w' || event.key === 'ArrowUp' ) movement.forward = - 1;
+					if ( event.key === 's' || event.key === 'ArrowDown' ) movement.forward = 1;
+					if ( event.key === 'a' || event.key === 'ArrowLeft' ) movement.right = 1;
+					if ( event.key === 'd' || event.key === 'ArrowRight' ) movement.right = - 1;
+					if ( event.key === 'r' ) movement.reset = true;
+					if ( event.key === ' ' ) movement.brake = 1;
+			
+				} );
+
+				window.addEventListener( 'keyup', ( event ) => {
+
+					if ( event.key === 'w' || event.key === 's' || event.key === 'ArrowUp' || event.key === 'ArrowDown' ) movement.forward = 0;
+					if ( event.key === 'a' || event.key === 'd' || event.key === 'ArrowLeft' || event.key === 'ArrowRight' ) movement.right = 0;
+					if ( event.key === 'r' ) movement.reset = false;
+					if ( event.key === ' ' ) movement.brake = 0;
+			
+				} );
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+			}
+
+			async function initPhysics() {
+
+				//Initialize physics engine using the script in the jsm/physics folder
+				physics = await RapierPhysics();
+
+				//Optionally display collider outlines
+				physicsHelper = new RapierHelper( physics.world );
+				scene.add( physicsHelper );
+
+				physics.addScene( scene );
+
+  				createCar();
+
+			}
+
+			function createCar( ) {
+
+				const geometry = new THREE.BoxGeometry( 2, 1, 4 );
+				const material = new THREE.MeshStandardMaterial( { color: 0xFF0000 } );
+				const mesh = new THREE.Mesh( geometry, material );
+				mesh.castShadow = true;
+				scene.add( mesh );
+				car = mesh;
+
+				mesh.position.y = 1;
+
+				physics.addMesh( mesh, 10, 0.8 ); // addMesh places the RigidBody in the mesh.userData.physics object
+				chassis = mesh.userData.physics.body;
+
+				vehicleController = physics.world.createVehicleController( chassis );
+
+				wheels = [];
+
+				addWheel( 0, { x: - 1, y: 0, z: - 1.5 }, mesh );
+				addWheel( 1, { x: 1, y: 0, z: - 1.5 }, mesh );
+				addWheel( 2, { x: - 1, y: 0, z: 1.5 }, mesh );
+				addWheel( 3, { x: 1, y: 0, z: 1.5 }, mesh );
+
+				vehicleController.setWheelSteering( 0, Math.PI / 4 );
+				vehicleController.setWheelSteering( 1, Math.PI / 4 );
+
+			}
+
+			function addWheel( index, pos, carMesh ) {
+
+				// Define wheel properties
+				const wheelRadius = 0.3;
+				const wheelWidth = 0.4;
+				const suspensionRestLength = 0.8;
+				const wheelPosition = pos; // Position relative to chassis
+				const wheelDirection = { x: 0.0, y: - 1.0, z: 0.0 }; // Downward direction
+				const wheelAxle = { x: - 1.0, y: 0.0, z: 0.0 }; // Axle direction
+
+				// Add the wheel to the vehicle controller
+				vehicleController.addWheel(
+					wheelPosition,
+					wheelDirection,
+					wheelAxle,
+					suspensionRestLength,
+					wheelRadius
+				);
+
+				// Set suspension stiffness for wheel
+				vehicleController.setWheelSuspensionStiffness( index, 24.0 );
+
+				// Set wheel friction
+				vehicleController.setWheelFrictionSlip( index, 1000.0 );
+
+				// Enable steering for the wheel
+				vehicleController.setWheelSteering( index, pos.z < 0 );
+
+				// Create a wheel mesh
+				const geometry = new THREE.CylinderGeometry( wheelRadius, wheelRadius, wheelWidth, 16 );
+				geometry.rotateZ( Math.PI * 0.5 );
+				const material = new THREE.MeshStandardMaterial( { color: 0x000000 } );
+				const wheel = new THREE.Mesh( geometry, material );
+
+				wheel.castShadow = true;
+
+				wheel.position.copy( pos );
+
+				wheels.push( wheel );
+				carMesh.add( wheel );
+
+			}
+
+			function updateWheels() {
+
+				if ( vehicleController === undefined ) return;
+
+				const wheelSteeringQuat = new THREE.Quaternion();
+				const wheelRotationQuat = new THREE.Quaternion();
+				const up = new THREE.Vector3( 0, 1, 0 );
+
+				//const chassisPosition = chassis.translation();
+
+				wheels.forEach( ( wheel, index ) => {
+
+					const wheelAxleCs = vehicleController.wheelAxleCs( index );
+					const connection = vehicleController.wheelChassisConnectionPointCs( index ).y || 0;
+					const suspension = vehicleController.wheelSuspensionLength( index ) || 0;
+					const steering = vehicleController.wheelSteering( index ) || 0;
+					const rotationRad = vehicleController.wheelRotation( index ) || 0;
+
+					wheel.position.y = connection - suspension;
+
+					wheelSteeringQuat.setFromAxisAngle( up, steering );
+					wheelRotationQuat.setFromAxisAngle( wheelAxleCs, rotationRad );
+
+					wheel.quaternion.multiplyQuaternions( wheelSteeringQuat, wheelRotationQuat );
+			
+				} );
+
+			}
+
+			function updateCarControl() {
+
+				if ( movement.reset ) {
+
+					chassis.setTranslation( new physics.RAPIER.Vector3( 0, 1, 0 ), true );
+					chassis.setRotation( new physics.RAPIER.Quaternion( 0, 0, 0, 1 ), true );
+					chassis.setLinvel( new physics.RAPIER.Vector3( 0, 0, 0 ), true );
+					chassis.setAngvel( new physics.RAPIER.Vector3( 0, 0, 0 ), true );
+
+					movement.accelerateForce.value = 0;
+					movement.brakeForce.value = 0;
+
+					return;
+
+				}
+
+				let accelerateForce = 0;
+
+				if ( movement.forward < 0 ) {
+
+					//if (movement.accelerateForce.value === 0) chassis.wakeUp();
+					accelerateForce = movement.accelerateForce.value - movement.accelerateForce.step;
+					if ( accelerateForce < movement.accelerateForce.min ) accelerateForce = movement.accelerateForce.min;
+			
+				} else if ( movement.forward > 0 ) {
+
+					//if (movement.accelerateForce.value === 0) chassis.wakeUp();
+					accelerateForce = movement.accelerateForce.value + movement.accelerateForce.step;
+			
+					if ( accelerateForce > movement.accelerateForce.max ) accelerateForce = movement.accelerateForce.max;
+			
+				} else {
+
+					if ( chassis.isSleeping() ) chassis.wakeUp();
+
+				}
+
+				movement.accelerateForce.value = accelerateForce;
+
+				//console.log(accelerateForce);
+
+				let brakeForce = 0;
+
+				if ( movement.brake > 0 ) {
+			
+					brakeForce = movement.brakeForce.value + movement.brakeForce.step;
+					if ( brakeForce > movement.brakeForce.max ) brakeForce = movement.brakeForce.max;
+
+				}
+
+				movement.brakeForce.value = brakeForce;
+
+				const engineForce = accelerateForce;
+
+				vehicleController.setWheelEngineForce( 0, engineForce );
+				vehicleController.setWheelEngineForce( 1, engineForce );
+
+				const currentSteering = vehicleController.wheelSteering( 0 );
+				const steerDirection = movement.right;
+				const steerAngle = Math.PI / 4;
+
+				const steering = THREE.MathUtils.lerp( currentSteering, steerAngle * steerDirection, 0.25 );
+
+				vehicleController.setWheelSteering( 0, steering );
+				vehicleController.setWheelSteering( 1, steering );
+
+				const wheelBrake = movement.brake * brakeForce;
+				vehicleController.setWheelBrake( 0, wheelBrake );
+				vehicleController.setWheelBrake( 1, wheelBrake );
+				vehicleController.setWheelBrake( 2, wheelBrake );
+				vehicleController.setWheelBrake( 3, wheelBrake );
+
+			}
+
+			function onWindowResize( ) {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+
+			function animate() {
+
+				if ( vehicleController ) {
+
+					updateCarControl();
+					vehicleController.updateVehicle( 1 / 60 );
+					updateWheels();
+
+				}
+
+				if ( controls && car ) {
+
+					controls.target.copy( car.position );
+					controls.update();
+
+				}
+
+				if ( physicsHelper ) physicsHelper.update();
+
+				renderer.render( scene, camera );
+
+				stats.update();
+
+			}
+
+		</script>
+	</body>
+</html>

BIN
examples/screenshots/physics_rapier_basic.jpg


BIN
examples/screenshots/physics_rapier_character_controller.jpg


BIN
examples/screenshots/physics_rapier_joints.jpg


BIN
examples/screenshots/physics_rapier_vehicle_controller.jpg


+ 4 - 0
examples/tags.json

@@ -9,6 +9,10 @@
 	"physics_ammo_cloth": [ "integration" ],
 	"physics_jolt_instancing": [ "external" ],
 	"physics_rapier_instancing": [ "external" ],
+	"physics_rapier_basic": [ "external" ],
+	"physics_rapier_joints": [ "external" ],
+	"physics_rapier_character_controller": [ "external" ],
+	"physics_rapier_vehicle_controller": [ "external" ],
 	"webgl_clipping": [ "solid" ],
 	"webgl_clipping_advanced": [ "solid" ],
 	"webgl_clipping_intersection": [ "solid" ],

+ 2 - 1
test/e2e/puppeteer.js

@@ -37,7 +37,7 @@ class PromiseQueue {
 
 /* CONFIG VARIABLES START */
 
-const idleTime = 9; // 9 seconds - for how long there should be no network requests
+const idleTime = 12; // 9 seconds - for how long there should be no network requests
 const parseTime = 6; // 6 seconds per megabyte
 
 const exceptionList = [
@@ -78,6 +78,7 @@ const exceptionList = [
 
 	// Unknown
 	// TODO: most of these can be fixed just by increasing idleTime and parseTime
+	'physics_rapier_basic',
 	'webgl_animation_skinning_blending',
 	'webgl_animation_skinning_additive_blending',
 	'webgl_buffergeometry_glbufferattribute',

粤ICP备19079148号