How can I create Breakable Text? (like in Ammo Js / Break) https://threejs.org/examples/?q=physics#physics_ammo_break

Hey Guys!
I’m a newbie, so please be gentle.

I am trying to create Breakable Text just like we can break the shapes given in the example below.

https://threejs.org/examples/?q=physics#physics_ammo_break

What is happening is that Text Geometry does not react normally to adding physics to it. It starts moving around in ways that make no sense, and uses a lot of resources.

Additionally I am trying to throw the ball through a Torus (A Hoop on a pole, like in Quidditch), but even the open part of the Torus acts solid.

Please help

Here’s my code: (I have just copy pasted the Ammo Js Break code and I am making the changes in it to achieve what I want, because I’m learning)

<html lang="en">
	<head>
		<title>Convex object breaking example</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">Physics threejs demo with convex objects breaking in real time<br />Press mouse to throw balls and move the camera.</div>
	<div id="container"></div>

	<script src="jsm/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.6.3/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';
		import { ConvexObjectBreaker } from 'three/addons/misc/ConvexObjectBreaker.js';
		import { ConvexGeometry } from 'three/addons/geometries/ConvexGeometry.js';
		import { FontLoader } from 'three/addons/loaders/FontLoader.js';
		import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
		import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
		// import { ConvexGeometry } from 'three/addons/geometries/ConvexGeometry.js';
		import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
		// import { QuickHull } from 'three/examples/js/QuickHull';
		// import { ConvexBufferGeometry } from 'three/examples/js/ConvexBufferGeometry';
		// import font from 'fonts/helvetiker_regular.typeface.json';
		// import { ConvexBufferGeometry, QuickHull } from '/examples/jsm/geometries/ConvexGeometry.js';

		let font;

		function loadFont() {

				const loader = new FontLoader();
				loader.load( 'fonts/helvetiker_regular.typeface.json', function ( response ) {

					font = response;

					refreshText();

				} );

			}

			loadFont();



		// - Global variables -

		// Graphics variables
		let container, stats;
		let camera, controls, scene, renderer;
		let textureLoader;
		const clock = new THREE.Clock();

		const mouseCoords = new THREE.Vector2();
		const raycaster = new THREE.Raycaster();
		const ballMaterial = new THREE.MeshPhongMaterial( { color: 0x202020 } );

		// Physics variables
		const gravityConstant = 7.8;
		let collisionConfiguration;
		let dispatcher;
		let broadphase;
		let solver;
		let physicsWorld;
		const margin = 0.05;

		const convexBreaker = new ConvexObjectBreaker();

		// Rigid bodies include all movable objects
		const rigidBodies = [];

		const pos = new THREE.Vector3();
		const quat = new THREE.Quaternion();
		let transformAux1;
		let tempBtVec3_1;

		const objectsToRemove = [];

		for ( let i = 0; i < 500; i ++ ) {

			objectsToRemove[ i ] = null;

		}

		let numObjectsToRemove = 0;

		const impactPoint = new THREE.Vector3();
		const impactNormal = new THREE.Vector3();

		// - Main code -

		Ammo().then( function ( AmmoLib ) {

			Ammo = AmmoLib;

			init();
			animate();

		} );


		// - Functions -

		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( - 14, 8, 16 );

			renderer = new THREE.WebGLRenderer();
			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( 0x707070 );
			scene.add( ambientLight );

			const light = new THREE.DirectionalLight( 0xffffff, 1 );
			light.position.set( - 10, 18, 5 );
			light.castShadow = true;
			const d = 14;
			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() {

			// 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, - gravityConstant, 0 ) );

			transformAux1 = new Ammo.btTransform();
			tempBtVec3_1 = new Ammo.btVector3( 0, 0, 0 );

		}

		// materials = [
		// 			new THREE.MeshPhongMaterial( { color: 0xffffff, flatShading: true } ), // front
		// 			new THREE.MeshPhongMaterial( { color: 0xffffff } ) // side
		// 		];

		function createObject( mass, halfExtents, pos, quat, material ) {

			const object = new THREE.Mesh( new THREE.BoxGeometry( halfExtents.x * 2, halfExtents.y * 2, halfExtents.z * 2 ), material );
			object.position.copy( pos );
			object.quaternion.copy( quat );
			convexBreaker.prepareBreakableObject( object, mass, new THREE.Vector3(), new THREE.Vector3(), true );
			// createDebrisFromBreakableObject( object );

		}

		function createObjects() {

			

			//Text
			console.log(font);
			const textGeo = new TextGeometry( 'HELLO', {
				font: font,
				size: 8,
				height: 0.5,
				curveSegments: 3,
				bevelThickness: 1,
				bevelSize: 0.1,
				bevelEnabled: true,
			} );
			
			textGeo.computeBoundingBox();
			
			const textMesh1 = new THREE.Mesh( textGeo, new THREE.MeshPhongMaterial( { color: 0xffffff, flatShading: true } ) );
			// console.log(textMesh1);
			pos.set( - 15, 0, 0 );
			quat.set( 0, 0, 0, 1 );
			textMesh1.position.copy( pos );
			textMesh1.quaternion.copy( quat );

			
			// Create the physics shape using Ammo.btConvexHullShape
			const textShape = new Ammo.btConvexHullShape();
			for (let i = 0; i < textGeo.attributes.position.count; i++) {
				textShape.addPoint(new Ammo.btVector3(
					textGeo.attributes.position.array[i * 3 + 0],
					textGeo.attributes.position.array[i * 3 + 1],
					textGeo.attributes.position.array[i * 3 + 2]
					));
				}
				
				// Create a rigid body using the physics shape and add it to the physics world
				
				const textBody = new Ammo.btRigidBody(new Ammo.btRigidBodyConstructionInfo(0, null, textShape, new Ammo.btVector3(0, 0, 0)));
				physicsWorld.addRigidBody(textBody);
				textMesh1.userData.physicsBody = textBody;
				createRigidBody( textMesh1, textShape, 100000, pos, quat );

	

			//ring 2 (Torus)
			const geometryRing = new THREE.TorusGeometry( 2, 0.5, 16, 100 );
			const materialRing = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
			const torus = new THREE.Mesh( geometryRing, materialRing );
			pos.set( 0, 5, 0 );
			torus.position.copy( pos );
			convexBreaker.prepareBreakableObject( torus, 1200, new THREE.Vector3(), new THREE.Vector3(), false );
			createDebrisFromBreakableObject( torus );
			// scene.add( torus );
		
		}

		function createParalellepipedWithPhysics( sx, sy, sz, mass, pos, quat, material ) {

			const object = 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( object, shape, mass, pos, quat );

			return object;

		}

		function createDebrisFromBreakableObject( object ) {

			object.castShadow = true;
			object.receiveShadow = true;

			const shape = createConvexHullPhysicsShape( object.geometry.attributes.position.array );
			shape.setMargin( margin );

			const body = createRigidBody( object, shape, object.userData.mass, null, null, object.userData.velocity, object.userData.angularVelocity );

			// Set pointer back to the three object only in the debris objects
			const btVecUserData = new Ammo.btVector3( 0, 0, 0 );
			btVecUserData.threeObject = object;
			body.setUserPointer( btVecUserData );

		}

		function removeDebris( object ) {

			scene.remove( object );

			physicsWorld.removeRigidBody( object.userData.physicsBody );

		}

		function createConvexHullPhysicsShape( coords ) {

			const shape = new Ammo.btConvexHullShape();

			for ( let i = 0, il = coords.length; i < il; i += 3 ) {

				tempBtVec3_1.setValue( coords[ i ], coords[ i + 1 ], coords[ i + 2 ] );
				const lastOne = ( i >= ( il - 3 ) );
				shape.addPoint( tempBtVec3_1, lastOne );

			}

			return shape;

		}

		function createRigidBody( object, physicsShape, mass, pos, quat, vel, angVel ) {

			if ( pos ) {

				object.position.copy( pos );

			} else {

				pos = object.position;

			}

			if ( quat ) {

				object.quaternion.copy( quat );

			} else {

				quat = object.quaternion;

			}

			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 );

			body.setFriction( 0.5 );

			if ( vel ) {

				body.setLinearVelocity( new Ammo.btVector3( vel.x, vel.y, vel.z ) );

			}

			if ( angVel ) {

				body.setAngularVelocity( new Ammo.btVector3( angVel.x, angVel.y, angVel.z ) );

			}

			object.userData.physicsBody = body;
			object.userData.collided = false;

			scene.add( object );

			if ( mass > 0 ) {

				rigidBodies.push( object );

				// Disable deactivation
				body.setActivationState( 4 );

			}

			physicsWorld.addRigidBody( body );

			return body;

		}

		function createRandomColor() {

			return Math.floor( Math.random() * ( 1 << 24 ) );

		}

		function createMaterial( color ) {

			color = color || createRandomColor();
			return new THREE.MeshPhongMaterial( { color: color } );

		}

		function initInput() {

			window.addEventListener( 'pointerdown', function ( event ) {

				mouseCoords.set(
					( event.clientX / window.innerWidth ) * 2 - 1,
					- ( event.clientY / window.innerHeight ) * 2 + 1
				);

				raycaster.setFromCamera( mouseCoords, camera );

				// Creates a ball and throws it
				const ballMass = 32;
				const ballRadius = 0.4;

				const ball = new THREE.Mesh( new THREE.SphereGeometry( ballRadius, 14, 10 ), 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 );

				pos.copy( raycaster.ray.direction );
				pos.multiplyScalar( 24 );
				ballBody.setLinearVelocity( new Ammo.btVector3( pos.x, pos.y, pos.z ) );

			} );

		}

		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 );

			renderer.render( scene, camera );

		}

		function updatePhysics( deltaTime ) {

			// Step world
			physicsWorld.stepSimulation( deltaTime, 10 );

			// Update rigid bodies
			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() );

					objThree.userData.collided = false;

				}

			}

			for ( let i = 0, il = dispatcher.getNumManifolds(); i < il; i ++ ) {

				const contactManifold = dispatcher.getManifoldByIndexInternal( i );
				const rb0 = Ammo.castObject( contactManifold.getBody0(), Ammo.btRigidBody );
				const rb1 = Ammo.castObject( contactManifold.getBody1(), Ammo.btRigidBody );

				const threeObject0 = Ammo.castObject( rb0.getUserPointer(), Ammo.btVector3 ).threeObject;
				const threeObject1 = Ammo.castObject( rb1.getUserPointer(), Ammo.btVector3 ).threeObject;

				if ( ! threeObject0 && ! threeObject1 ) {

					continue;

				}

				const userData0 = threeObject0 ? threeObject0.userData : null;
				const userData1 = threeObject1 ? threeObject1.userData : null;

				const breakable0 = userData0 ? userData0.breakable : false;
				const breakable1 = userData1 ? userData1.breakable : false;

				const collided0 = userData0 ? userData0.collided : false;
				const collided1 = userData1 ? userData1.collided : false;

				if ( ( ! breakable0 && ! breakable1 ) || ( collided0 && collided1 ) ) {

					continue;

				}

				let contact = false;
				let maxImpulse = 0;
				for ( let j = 0, jl = contactManifold.getNumContacts(); j < jl; j ++ ) {

					const contactPoint = contactManifold.getContactPoint( j );

					if ( contactPoint.getDistance() < 0 ) {

						contact = true;
						const impulse = contactPoint.getAppliedImpulse();

						if ( impulse > maxImpulse ) {

							maxImpulse = impulse;
							const pos = contactPoint.get_m_positionWorldOnB();
							const normal = contactPoint.get_m_normalWorldOnB();
							impactPoint.set( pos.x(), pos.y(), pos.z() );
							impactNormal.set( normal.x(), normal.y(), normal.z() );

						}

						break;

					}

				}

				// If no point has contact, abort
				if ( ! contact ) continue;

				// Subdivision

				const fractureImpulse = 250;

				if ( breakable0 && ! collided0 && maxImpulse > fractureImpulse ) {

					const debris = convexBreaker.subdivideByImpact( threeObject0, impactPoint, impactNormal, 1, 2, 1.5 );

					const numObjects = debris.length;
					for ( let j = 0; j < numObjects; j ++ ) {

						const vel = rb0.getLinearVelocity();
						const angVel = rb0.getAngularVelocity();
						const fragment = debris[ j ];
						fragment.userData.velocity.set( vel.x(), vel.y(), vel.z() );
						fragment.userData.angularVelocity.set( angVel.x(), angVel.y(), angVel.z() );

						createDebrisFromBreakableObject( fragment );

					}

					objectsToRemove[ numObjectsToRemove ++ ] = threeObject0;
					userData0.collided = true;

				}

				if ( breakable1 && ! collided1 && maxImpulse > fractureImpulse ) {

					const debris = convexBreaker.subdivideByImpact( threeObject1, impactPoint, impactNormal, 1, 2, 1.5 );

					const numObjects = debris.length;
					for ( let j = 0; j < numObjects; j ++ ) {

						const vel = rb1.getLinearVelocity();
						const angVel = rb1.getAngularVelocity();
						const fragment = debris[ j ];
						fragment.userData.velocity.set( vel.x(), vel.y(), vel.z() );
						fragment.userData.angularVelocity.set( angVel.x(), angVel.y(), angVel.z() );

						createDebrisFromBreakableObject( fragment );

					}

					objectsToRemove[ numObjectsToRemove ++ ] = threeObject1;
					userData1.collided = true;

				}

			}

			for ( let i = 0; i < numObjectsToRemove; i ++ ) {

				removeDebris( objectsToRemove[ i ] );

			}

			numObjectsToRemove = 0;

		}

		</script>

	</body>
</html>

today i learn, i had no idea this even exists in three/esamples/jsm :exploding_head:

sorry for not having a solution but just marveling at the possibilities

i’ve tried it quiet-mountain-bi1cvj - CodeSandbox though this is rapier, but it looks like it really can only handle simple convex shapes, so you won’t get detailed shards.

1 Like