Skip to content

Integrating with Three.js

William Mcmurray edited this page Sep 16, 2018 · 2 revisions

By itself Goblin physics doesn't have any kind of graphical output, it only simulates how objects move and react to collisions. To visualize the system you need a way to render the physics world; this example shows how to do so with Three.js. You are not limited to using Three.js, but it is a popular and easy to use WebGL library making it a good choice.

Before starting, make sure you have read the Getting Started part of the Goblin wiki.

The Parts

To integrate Goblin & Three.js we will need a few things:

  • physics world
  • renderable scene
  • mapping of rigid bodies in Goblin to renderable objects in Three.js
  • method that runs the physics simulation, updates the Three.js scene, and performs a render

Physics World

We'll reuse the setup created in Getting Started, with one dynamic box and a static box.

var world = new Goblin.World( new Goblin.BasicBroadphase(), new Goblin.NarrowPhase(), new Goblin.IterativeSolver() );

var box_shape = new Goblin.BoxShape( 0.5, 0.5, 0.5 ), // dimensions are half width, half height, and half depth, or a box 1x1x1
    mass = 5,
    dynamic_box = new Goblin.RigidBody( box_shape, mass );
world.addRigidBody( dynamic_box );

var static_box = new Goblin.RigidBody( box_shape, Infinity ); // Mass of Infinity means the box cannot move
static_box.position.set( 0, -5, 0 ); // Set the static box's position 5 units down
world.addRigidBody( static_box );

Renderable Scene

A scene in Three.js requires a couple things: a scene object containing all of the objects, a camera, a renderer, and a light to make things look nice. The following code setups up a scene to mimic the physics world created above.

var scene = new THREE.Scene(),
    camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 100 ), // FOV, aspect, near, far
    renderer = new THREE.WebGLRenderer({ antialias: true });

var box_geometry = new THREE.BoxGeometry( 1, 1, 1 ), // note that the `BoxGeometry` arguments are the box's full width, height, and depth, while the parameters for `Goblin.BoxShape` are expressed as half sizes
    box_material = new THREE.MeshLambertMaterial({ color: 0xaa8833 }),
    dynamic_mesh = new THREE.Mesh( box_geometry, box_material ),
    static_mesh = new THREE.Mesh( box_geometry, box_material );

// Add both mesh's to the scene
scene.add( dynamic_mesh );

static_mesh.position.set( 0, -5, 0 );
scene.add( static_mesh );

// Setup the camera
camera.position.set( 5, 5, 5 );
camera.lookAt( static_mesh.position );

// Setup the renderer
renderer.setSize( window.innerWidth, window.innerHeight);
document.body.appendChild( renderer.domElement );

// Setup the light
var light = new THREE.PointLight({ color: 0xffffff });
light.position.copy( camera.position );
scene.add( light );

Mapping Goblin -> Three.js

We need a way to keep track of which mesh each rigid body corresponds to, otherwise there is no way to update the meshes' position. Each rigid body has a unique id property that can be used as a key to point to a mesh.

var body_to_mesh_map = {};
body_to_mesh_map[dynamic_box.id] = dynamic_mesh;
body_to_mesh_map[static_box.id] = static_mesh;

If you are wondering why the static_box -> static_mesh map is included, it doesn't need to be as the static_mesh is already in the correct position. It's included as an example.

Run Simulation, Update, Render

The update & render loop will target 60fps and process everything as part of the same loop. It is encouraged that bigger projects separate the physics and render loops for performance and usability purposes.

function updateAndRender() {
	window.requestAnimationFrame( updateAndRender );

	// run physics simulation
	world.step( 1 / 60 );

	// update mesh positions / rotations
	for ( var i = 0; i < world.rigid_bodies.length; i++ ) {
		var body = world.rigid_bodies[i],
			mesh = body_to_mesh_map[ body.id ];

		// update position
		mesh.position.x = body.position.x;
		mesh.position.y = body.position.y;
		mesh.position.z = body.position.z;

		// update rotation
		mesh.quaternion._x = body.rotation.x;
		mesh.quaternion._y = body.rotation.y;
		mesh.quaternion._z = body.rotation.z;
		mesh.quaternion._w = body.rotation.w;
	}

	// render
	renderer.render( scene, camera );
}
window.requestAnimationFrame( updateAndRender );

Everything Together

<!DOCTYPE html>
<html>
<head>
	<script type="text/javascript" src="goblin.min.js"></script>
	<script type="text/javascript" src="three.min.js"></script>
</head>
<body>
	<script type="text/javascript">
		var world = new Goblin.World( new Goblin.BasicBroadphase(), new Goblin.NarrowPhase(), new Goblin.IterativeSolver() );

		var box_shape = new Goblin.BoxShape( 0.5, 0.5, 0.5 ), // dimensions are half width, half height, and half depth, or a box 1x1x1
		    mass = 5,
		    dynamic_box = new Goblin.RigidBody( box_shape, mass );
		world.addRigidBody( dynamic_box );

		var static_box = new Goblin.RigidBody( box_shape, Infinity ); // Mass of Infinity means the box cannot move
		static_box.position.set( 0, -5, 0 ); // Set the static box's position 5 units down
		world.addRigidBody( static_box );

		var scene = new THREE.Scene(),
		    camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 100 ), // FOV, aspect, near, far
		    renderer = new THREE.WebGLRenderer({ antialias: true });

		var box_geometry = new THREE.BoxGeometry( 1, 1, 1 ), // note that the `BoxGeometry` arguments are the box's full width, height, and depth, while the parameters for `Goblin.BoxShape` are expressed as half sizes
		    box_material = new THREE.MeshLambertMaterial({ color: 0xaa8833 }),
		    dynamic_mesh = new THREE.Mesh( box_geometry, box_material ),
		    static_mesh = new THREE.Mesh( box_geometry, box_material );

		// Add both mesh's to the scene
		scene.add( dynamic_mesh );

		static_mesh.position.set( 0, -5, 0 );
		scene.add( static_mesh );

		// Setup the camera
		camera.position.set( 8, 0, 8 );
		camera.lookAt( static_mesh.position );

		// Setup the renderer
		renderer.setSize( window.innerWidth, window.innerHeight);
		renderer.setClearColor( 0xffffff );
		document.body.appendChild( renderer.domElement );

		// Setup the light
		var light = new THREE.PointLight({ color: 0xffffff });
		light.position.copy( camera.position );
		scene.add( light );

		var body_to_mesh_map = {};
		body_to_mesh_map[dynamic_box.id] = dynamic_mesh;
		body_to_mesh_map[static_box.id] = static_mesh;

		function updateAndRender() {
    		window.requestAnimationFrame( updateAndRender );

    		// run physics simulation
    		world.step( 1 / 60 );

    		// update mesh positions / rotations
    		for ( var i = 0; i < world.rigid_bodies.length; i++ ) {
    			var body = world.rigid_bodies[i],
    				mesh = body_to_mesh_map[ body.id ];

    			// update position
    			mesh.position.x = body.position.x;
    			mesh.position.y = body.position.y;
    			mesh.position.z = body.position.z;

    			// update rotation
    			mesh.quaternion._x = body.rotation.x;
    			mesh.quaternion._y = body.rotation.y;
    			mesh.quaternion._z = body.rotation.z;
    			mesh.quaternion._w = body.rotation.w;
    		}

    		// render
    		renderer.render( scene, camera );
		}
		window.requestAnimationFrame( updateAndRender );
	</script>
</body>
</html>