Using Three.js and JigLibJS Together
For whatever reason, one of my projects at my current employer led me to a need for doing some 3D work in HTML5′s canvas element. After a few hack-ish, amateur attempts at repurposing what I’d done for Mega Hovertank Wars, I decided to use something with a community behind it and found three.js from Mr.doob, a man you may have heard of through his work on Arcade Fire’s HTML5 experiment “The Wilderness Downtown.” There are plenty of other options out there (including some that use WebGL rather than a pure canvas renderer), but I think after perusing its examples and reading Seb Lee-Delisle’s blog, I’d settled on three.js.
By modifying the geometry_cube.html example, I arrived at this:
All I did was change the face materials of the starting cube and used that materials array to color the 5 flaps below (along with changing some Camera() and Cube() dimension and position parameters, but those are pretty obvious):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | camera = new THREE.Camera( 45, window.innerWidth / window.innerHeight, 1, 3000 ); camera.position.y = 50; camera.position.z = 1500; camera.target.position.y = 50; scene = new THREE.Scene(); // Cube var materials = []; materials.push( [ new THREE.MeshBasicMaterial( { color: 0x0000ff } ) ] ); // Left side materials.push( [ new THREE.MeshBasicMaterial( { color: 0x666666 } ) ] ); // Right side materials.push( [ new THREE.MeshBasicMaterial( { color: 0xff0000 } ) ] ); // Top side materials.push( [ new THREE.MeshBasicMaterial( { color: 0x00ff00 } ) ] ); // Bottom side materials.push( [ new THREE.MeshBasicMaterial( { color: 0xcccccc } ) ] ); // Front side materials.push( [ new THREE.MeshBasicMaterial( { color: 0x000000 } ) ] ); // Back side cube = new THREE.Mesh( new Cube( 925, 460, 10, 1, 1, materials ), new THREE.MeshFaceMaterial() ); cube.position.y = 150; cube.overdraw = true; scene.addObject( cube ); // Flap for(var i = 0; i < offset.length; i++) { flap[i] = new THREE.Mesh( new Cube( 160, 160, 10, 1, 1, materials ), new THREE.MeshFaceMaterial() ); flap[i].autoUpdateMatrix = false; flap[i].overdraw = true; flap[i].position.y = flap[i].originalY = (i % 2 == 1 ? -210 : -250); flap[i].position.x = offset[i]; scene.addObject(flap[i]); } |
That was the simple part. The only true oddity is setting the autoUpdateMatrix attribute of each square to false. This is because if you don’t do this, you can’t manually modify each square’s matrix later on. Also, if you notice, when you drag around on the example, the cube spins, and I wanted to incorporate that into the “flaps” animations.
Relative Animation
Well, that’s what I’m calling it anyways. What it really means is that I based the “flapping” intensity on the difference between the main panel’s targetRotation and current rotation (cube.rotation.y).
The physics I was hoping to achieve was something that looked like there is a rope between the panel and the squares and they flail in the wind behind the panel as it rotates. Something like this:
It looks pretty good considering the code is based mostly on basic advanced math.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | var cval = Math.cos(cube.rotation.y); var sval = Math.sin(cube.rotation.y); var rot = (targetRotation - cube.rotation.y) * 0.6; var dir = new THREE.Vector3(cval, 0, -sval); for(var i = 0; i < flap.length; i++) { var nrot = rot * Math.abs(offset[i] / 382.5); if(nrot > PI2) nrot =PI2; else if(nrot < -PI2) nrot = -PI2; flap[i].position.x = offset[i] * cval; flap[i].position.z = -offset[i] * sval; if(offset[i] != 0) { flap[i].position.y = (flap[i].originalY + 80) - Math.abs(Math.cos(nrot) * 80); var rval = Math.sin(nrot) * flap[i].originalY;// flap[i].originalY for trailing, flap[i].height/2 (or 80) for hinge flap[i].position.x += rval * sval * -(offset[i] / Math.abs(offset[i])); flap[i].position.z += rval * cval * -(offset[i] / Math.abs(offset[i])); } alignCube(flap[i], dir.normalize(), (offset[i] < 0 ? 1 : -1) * nrot); } |
The first key is that in the lines where I’m setting each flap’s position.y, position.x and position.z values, that is to give the “roped” effect. All I’m really doing is adding to each position coordinate as if it were rotating on a hinge. Simple math!
The second key is that you are determining the vector pointing along the front face of the panel at any given point of the rotation by taking the Math.cos() and Math.sin() values of the current rotational value of the panel. This allows us to find the axis we need to rotate each square.
3D Rotation Along An Arbitrary Axis
However, doing this was harder than I thought it would be. After some quick Googling, however, I found an extremely helpful Stack Overflow post and simply adapted some of the help in the discussion for my own purposes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function alignCube(target, dir, rot) { var up = new THREE.Vector3(1, 0, 0); var angle = Math.acos(up.dot(dir)); var axis = new THREE.Vector3(); axis.cross(up, dir); axis.normalize(); var position = THREE.Matrix4.translationMatrix( target.position.x, target.position.y, target.position.z ); var rotate = THREE.Matrix4.rotationAxisAngleMatrix(axis, angle); var revolve = THREE.Matrix4.rotationAxisAngleMatrix(dir, rot); var scale = THREE.Matrix4.scaleMatrix( 1, 1, 1 ); revolve.multiplySelf(rotate); position.multiplySelf(revolve); target.matrix = position; } |
To simplify, you want to find the axis and angle that will get the original vector to match the desired vector, cross-multiply them, and normalize. That will allow you to form the rotation and revolution matrices which you can then multiply with the position matrix (the ordering of which is important, i.e. (revolve x rotate) x position) which results in the final position and orientation matrix.
Integrating JigLibJS Physics
Unfortunately, this wasn’t as realistic as I’d hoped. I could have done more tweaking to avoid some faults against true physics (such as when you abruptly change directions but only to stop rotation, the squares don’t swing back and forth in a natural fashion), but the end goal for this little side project was much more grand to the point where it just made more sense to use real physics.
Always interested by physics engines such as Havok, a popular choice for video game developers, I started to browse. I first arrived at a port of the Bullet Physics Library for JavaScript by Pl4n3, but it was incredibly buggy. Object would collide and bounce and almost always freeze. I love the effort, but bullet.js was a definite no-go.
Remembering a physics library called JigLib that was ported to Flash from Java, I hoped that someone had done the same for JavaScript, and luckily someone had! JigLibJS was functional, actively developed, and—most importantly—stable.
If you’re not aware (and I wasn’t prior to this little excursion, though in hindsight, it makes total sense), physics engines work by pretty much having you define objects from your rendered model world in the physics world. You then access the objects in the physics engine and take their position and orientations and apply them to the rendering engine, so this is what I did.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var system = jigLib.PhysicsSystem.getInstance(); system.setGravity([0,-9.8,0,0]);//-120 system.setSolverType('ACCUMULATED');//FAST, NORMAL, ACCUMULATED var ground = new jigLib.JPlane(null,[0, 1, 0, 0]); ground.set_friction(10); system.addBody(ground); ground.moveTo([0,-650,0,0]); panel = new jigLib.JBox(null, 925, 10, 460); panel.set_mass(50); panel.moveTo([0, 150, 0, 0]); panel.set_movable(false); system.addBody(panel); |
JigLibJS is pretty intuitive. The first two lines are system-wide parameters in that everything works on a -9.8 units per second per second gravity and uses the accumulated solver type (it’s the most accurate but is also the slowest of the three).
JigLibJS automatically considers JPlane() objects to be immovable and can thus easily be added to be ground and wall boundaries in the physics system. Other items like boxes are defined and moved about in a manner similar to three.js, so that was handy. One key line for the panel variable is the set_movable(false) so that it isn’t not going to move about under gravity or as other JigLibJS objects collide with it.
I simply repeated that for each square that I wanted and voila, an accurate representation of my models in JigLibJS!
Putting three.js and JigLibJS Together
At this point it was really just taking one matrix (from JigLibJS) and setting the three.js matrix to it. The only weird thing is that I chose to orient the JigLibJS representation of the panel to the three.js representation while the squares went the other way around, i.e. from three.js to JigLibJS. This becomes clear later when I do the world constraints.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | cube.rotation.y += ( targetRotation - cube.rotation.y ) * 0.05; panel.set_rotationY( cube.rotation.y * (180/Math.PI) ); now = new Date().getTime(); system.integrate((now - then) / 75);//400 then = now; for(var i = 0; i < offset.length; i++) { var pos = obj[i].get_currentState().position; var dir = obj[i].get_currentState().get_orientation().glmatrix; var position = THREE.Matrix4.translationMatrix( pos[0], pos[1], pos[2] ); var rotate = new THREE.Matrix4(dir[0], dir[1], dir[2], dir[3], dir[4], dir[5], dir[6], dir[7], dir[8], dir[9], dir[10], dir[11], dir[12], dir[13], dir[14], dir[15]); position.multiplySelf(rotate); flap[i].matrix = position; flap[i].update(false, true, camera); } |
It’s very similar to the alignCube() function we had before, except the trouble of turning an axis and an angle into a matrix is done for us prior by JigLibJS. The result is some square falling from underneath the panel and laying there. Exciting, right?
But remember? I want them to hang like they’re on ropes, not fall straight to the ground!
JigLibJS Constraints
Luckily, JigLibJS has these things called constraints. The unfortunate thing is that they don’t work exactly as you would imagine. Whereas the JConstraintMaxDistance seems like it should work (or even the JConstraintPoint), they both have a slight…elasticity to their constraining action and it can’t really be avoided. I even tried out the spring simulation solution, but, surprise surprise, it behaved more like a spring than a rope.
Instead, I used JConstraintWorldPoint. It still has a slight stretch to the constraint distance, but it holds pretty well so long as you don’t spin the panel too fast.
1 2 | con[i] = new jigLib.JConstraintWorldPoint(obj[i], [0,80,0,0], [offset[i], (i % 2 == 1 ? -130 : -170), 0, 0]); system.addConstraint(con[i]); |
This little piece of code was simply added into the loop I had going for when I was creating each square in JigLibJS. It simply adds the constraint to the physics system.
However, since these constraints are simply bound to a point in the world (this is where using JConstraintMaxDistance would have been much simpler), you have to manually move each point that each square is bound to. Luckily, I’d previous solved this little puzzle back when I was doing relative animation.
1 2 3 4 5 | var cval = Math.cos(cube.rotation.y); var sval = Math.sin(cube.rotation.y); for(var i = 0; i < offset.length; i++) { con[i].set_worldPosition([offset[i]*cval, flap[i].originalY+80, -offset[i]*sval, 0]); } |
That takes place within the animation loop and simply iterates over each constraint and moves it according to the orientation of the three.js representation of the panel. If it all goes according to plan, it should look something like this when spun.
Yeah, it doesn’t look like they’re on ropes, but hey, it’s a lot closer than I was a few days ago.
View the demo (requires an HTML5-capable browser). You can use the up and down arrow keys to move the camera and click the squares to add and remove their respective JigLibJS constraints. The panel also auto-aligns every five seconds, so don’t freak out when things move without you doing anything.
Looks really interesting, in retrospect how well did it work and if you had to redo the project now, would you use the same js physics library?
Probably not. I mean, JigLibJS is pretty good and all, but now that I better understand how physics engines work with the rest of an application, I think I’d like to give it a try writing my own.
If I was left to choose a pre-made and ready-to-go library, though, I’d probably stick with JigLib. It’s one of the faster libraries and doesn’t suffer from stuttering objects as much as others.
[...] which in itself is a port of Jiglib for c++. It seemed pretty promising, since there was some people using it (along with three.js), and I had great success with using physics libs in the past for 2d [...]
[...] one stage I was considering the use of the JigLibJS library as discussed on Tim Poon’s site. Didn’t go down this road in the end but could be [...]
Thanx a lot! Very good report about an easy issue with the right tools! Merry x-mas and a Happy new year