My esteemed colleagues over in the ADN team, Philippe and Balaji, have been working their magic, creating samples to show how to make use of JavaScript-based physics engines within Autodesk software. They’ve inspired me to have a go myself.
Philippe’s sample – which carried on from his preliminary research into JavaScript-based physics engines – shows how you can integrate ammo.js with the View & Data API to add some gravity to an A360 model. Really fun stuff!
[Side note: I love that ammo.js stands for “Avoid Making My Own physics engine” as well as being an emscripten port of the popular Bullet physics engine. That’s a truly excellent acronym. :-)]
Balaji’s sample is more AutoCAD-centric. He uses Physijs – which is based on ammo.js but targets Three.js specifically – to add Physics to an AutoCAD-hosted JavaScript app. As a starting point he took my Three.js sample, extending it to show how collision detection can be used with an AutoCAD model – albeit with the heavy lifting performed inside an HTML palette – to model the rotation of a swing arm. Balaji’s background – before software – is as a mechanical engineer, so this is bread and butter for him, of course.
I’m basically a software engineer, which no doubt makes me much more frivolous. Rather than building a useful sample using Physijs, as Balaji has done, I decided to integrate one of the existing Physijs samples inside AutoCAD, and to have a bit of fun of my own at the same time.
Of the various demo samples posted on the Physijs site, the one that stands out for me is the Jenga sample. All my family loves that game. While the demo doesn’t really include any gameplay, per se – it’s just a scene you can play around with – it feels very realistic. A nice demo of the engine’s capabilities when combined with Three.js.
I decided to take that sample as is, but integrate it into an AutoCAD palette and provide the ability to capture the graphics in the scene, bringing the various blocks into the current drawing as Solid3d objects.
Here’s how the integration ended up working:
Here's the C# code, which is fairly unexceptional – the main trick being to apply the Euler angles in the right order when building our transformation matrix. Three.js is Y-up rather than Z-up, so this took me some work to get right.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace JavaScriptSamples
{
public class PhysicsCommands
{
private PaletteSet _pps = null;
private static Document _curDoc = null;
[DllImport(
"AcJsCoreStub.crx", CharSet = CharSet.Auto,
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "acjsInvokeAsync")]
extern static private int acjsInvokeAsync(
string name, string jsonArgs
);
[CommandMethod("JENGA")]
public void JengaPalette()
{
_curDoc =
Application.DocumentManager.MdiActiveDocument;
_pps =
Utils.ShowPalette(
_pps,
new Guid("ABDC33C6-5F7C-4366-B171-2F824097CD4B"),
"JENGA",
"Physi.js Examples",
GetHtmlPathJenga()
);
_pps.SizeChanged += OnPaletteSizeChanged;
}
[CommandMethod("JENGADOC")]
public void JengaDocument()
{
_curDoc = Application.DocumentManager.MdiActiveDocument;
if (_curDoc != null)
{
_curDoc.BeginDocumentClose += (s, e) => _curDoc = null;
}
Application.DocumentWindowCollection.AddDocumentWindow(
"Physi.js Document", GetHtmlPathJenga()
);
}
// Create some classes to simplify de-serializing JSON
// from our web-page
public class Vector3
{
public double x { get; set; }
public double y { get; set; }
public double z { get; set; }
}
public class SolidInfo
{
public Vector3 position { get; set; }
public Vector3 rotation { get; set; }
public double length { get; set; }
public double height { get; set; }
public double width { get; set; }
}
[JavaScriptCallback("JengaSolids")]
public string JengaSolids(string jsonArgs)
{
var res = "{\"retCode\":1}";
var doc =
Application.DocumentManager.MdiActiveDocument;
if (doc == null)
doc = _curDoc;
if (doc == null)
return res;
var ed = doc.Editor;
var db = doc.Database;
using (var l = doc.LockDocument())
{
using (var tr = doc.TransactionManager.StartTransaction())
{
var bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead,
false
);
var btr =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForWrite,
false
);
var ja = JArray.Parse(jsonArgs);
var sols = ja.ToObject<List<SolidInfo>>();
foreach (var sol in sols)
{
double length = sol.length,
width = sol.width,
height = sol.height;
var dbs = new Solid3d();
dbs.CreateBox(length, width, height);
var p = sol.position;
var c = Point3d.Origin;
var disp =
Matrix3d.Displacement(new Vector3d(p.x, p.y, p.z));
var rotx =
Matrix3d.Rotation(sol.rotation.x, Vector3d.XAxis, c);
var roty =
Matrix3d.Rotation(sol.rotation.y, Vector3d.YAxis, c);
var rotz =
Matrix3d.Rotation(sol.rotation.z, Vector3d.ZAxis, c);
// Combine them following the "XYZ" order used in
// Three.js (although z and y are flipped)
var mat =
roty.PreMultiplyBy(
rotz.PreMultiplyBy(
rotx.PreMultiplyBy(disp)
)
);
dbs.TransformBy(mat);
btr.AppendEntity(dbs);
tr.AddNewlyCreatedDBObject(dbs, true);
}
tr.Commit();
}
}
return "{\"retCode\":0}";
}
private void OnPaletteSizeChanged(
object s, PaletteSetSizeEventArgs e
)
{
if (_pps != null && _pps.Count > 0)
{
acjsInvokeAsync("refresh", "{}");
}
}
private static Uri GetHtmlPathJenga()
{
return new Uri(
//Utils.GetHtmlPath() +
"http://through-the-interface.typepad.com/files/" +
"jenga/jenga.html"
);
}
}
}
Of more interest, overall, is the HTML file. Most of which comes from the Physijs sample verbatim, of course. The main piece I’ve added is the ability to package up the blocks from the scene as JSON for sending over to AutoCAD.
<!DOCTYPE html>
<html>
<head>
<title>Jenga - Physijs</title>
<style>
html {
margin: 0;
padding: 0;
}
body {
margin: 0;
padding: 0;
font-family: Verdana;
font-size: 10pt;
overflow: hidden;
}
#viewport {
position: relative;
}
#heading {
position: absolute;
bottom: 0;
width: 100%;
margin: 0;
padding: 0;
background-color: #71b87b;
text-align: center;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
}
#heading h1 {
margin: 0;
padding: 6px 0;
background-color: #64a36d;
font-size: 16px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
vertical-align: bottom;
}
</style>
<script type="text/javascript" src="js/three.min.js"></script>
<script type="text/javascript" src="js/stats.js"></script>
<script type="text/javascript" src="js/physi.js"></script>
<script
src="http://app.autocad360.com/jsapi/v2/Autodesk.AutoCAD.js">
</script>
<script type="text/javascript">
'use strict';
Physijs.scripts.worker = 'js/physijs_worker.js';
Physijs.scripts.ammo = 'ammo.js';
function sendBlocksToAutoCAD(jsonArgs) {
var jsonResponse =
exec(
JSON.stringify({
functionName: 'JengaSolids',
invokeAsCommand: false,
functionParams: jsonArgs
})
);
var jsonObj = JSON.parse(jsonResponse);
if (jsonObj.retCode !== Acad.ErrorStatus.eJsOk) {
throw Error(jsonObj.retErrorString);
}
return jsonObj.result;
}
var sendBlocks, initScene, initEventHandling, render,
createTower, renderer, scene,
dir_light, am_light, camera, table, blocks = [],
table_material, block_material, intersect_plane,
selected_block = null, mouse_position = new THREE.Vector3,
block_offset = new THREE.Vector3, _i,
_v3 = new THREE.Vector3;
sendBlocks = function () {
if (typeof exec !== "undefined") {
var objects = [], obj, block;
for (var i=0; i < blocks.length; i++) {
block = blocks[i];
obj = new Object();
obj.position = {};
obj.position.x = block.position.x;
obj.position.y = -block.position.z;
obj.position.z = block.position.y;
obj.rotation = {};
obj.rotation.x = block.rotation.x;
obj.rotation.y = -block.rotation.z;
obj.rotation.z = block.rotation.y;
var ext = block.geometry.boundingBox;
obj.length = Math.abs(ext.max.x - ext.min.x);
obj.height = Math.abs(ext.max.y - ext.min.y);
obj.width = Math.abs(ext.max.z - ext.min.z);
objects.push(obj);
}
sendBlocksToAutoCAD(objects);
}
};
initScene = function() {
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMapEnabled = true;
renderer.shadowMapSoft = true;
var vp = document.getElementById('viewport');
vp.appendChild(renderer.domElement);
scene = new Physijs.Scene({ fixedTimeStep: 1 / 120 });
scene.setGravity(new THREE.Vector3(0, -30, 0));
scene.addEventListener(
'update',
function() {
if (selected_block !== null) {
_v3.copy(mouse_position).add(block_offset).
sub(selected_block.position).multiplyScalar(5);
_v3.y = 0;
selected_block.setLinearVelocity(_v3);
// Reactivate all of the blocks
_v3.set(0, 0, 0);
for (_i = 0; _i < blocks.length; _i++) {
blocks[_i].applyCentralImpulse(_v3);
}
}
scene.simulate(undefined, 1);
}
);
camera = new THREE.PerspectiveCamera(
35,
window.innerWidth / window.innerHeight,
1,
1000
);
camera.position.set(25, 20, 25);
camera.lookAt(new THREE.Vector3(0, 7, 0));
scene.add(camera);
// ambient light
am_light = new THREE.AmbientLight(0x444444);
scene.add(am_light);
// directional light
dir_light = new THREE.DirectionalLight(0xFFFFFF);
dir_light.position.set(20, 30, -5);
dir_light.target.position.copy(scene.position);
dir_light.castShadow = true;
dir_light.shadowCameraLeft = -30;
dir_light.shadowCameraTop = -30;
dir_light.shadowCameraRight = 30;
dir_light.shadowCameraBottom = 30;
dir_light.shadowCameraNear = 20;
dir_light.shadowCameraFar = 200;
dir_light.shadowBias = -.001
dir_light.shadowMapWidth = dir_light.shadowMapHeight = 2048;
dir_light.shadowDarkness = .5;
scene.add(dir_light);
// Materials
table_material = Physijs.createMaterial(
new THREE.MeshLambertMaterial({
map: THREE.ImageUtils.loadTexture('images/wood.jpg'),
ambient: 0xFFFFFF
}),
.9, // high friction
.2 // low restitution
);
table_material.map.wrapS =
table_material.map.wrapT = THREE.RepeatWrapping;
table_material.map.repeat.set(5, 5);
block_material = Physijs.createMaterial(
new THREE.MeshLambertMaterial({
map: THREE.ImageUtils.loadTexture('images/plywood.jpg'),
ambient: 0xFFFFFF
}),
.4, // medium friction
.4 // medium restitution
);
block_material.map.wrapS =
block_material.map.wrapT = THREE.RepeatWrapping;
block_material.map.repeat.set(1, .5);
// Table
table = new Physijs.BoxMesh(
new THREE.BoxGeometry(50, 1, 50),
table_material,
0, // mass
{ restitution: .2, friction: .8 }
);
table.position.y = -.5;
table.receiveShadow = true;
scene.add(table);
createTower();
intersect_plane = new THREE.Mesh(
new THREE.PlaneGeometry(150, 150),
new THREE.MeshBasicMaterial({
opacity: 0, transparent: true
})
);
intersect_plane.rotation.x = Math.PI / -2;
scene.add(intersect_plane);
initEventHandling();
requestAnimationFrame(render);
scene.simulate();
};
render = function() {
requestAnimationFrame(render);
renderer.render(scene, camera);
};
createTower = (function() {
var block_length = 6, block_height = 1, block_width = 1.5,
block_offset = 2,
block_geometry =
new THREE.BoxGeometry(
block_length, block_height, block_width
);
return function() {
var i, j, rows = 16, block;
for (i = 0; i < rows; i++) {
for (j = 0; j < 3; j++) {
block =
new Physijs.BoxMesh(block_geometry, block_material);
block.position.y =
(block_height / 2) + block_height * i;
if (i % 2 === 0) {
// #TODO:
// There's a bug somewhere when this is too close to 2
block.rotation.y = Math.PI / 2.01;
block.position.x =
block_offset * j -
(block_offset * 3 / 2 - block_offset / 2);
} else {
block.position.z =
block_offset * j -
(block_offset * 3 / 2 - block_offset / 2);
}
block.receiveShadow = true;
block.castShadow = true;
scene.add(block);
blocks.push(block);
}
}
}
})();
initEventHandling = (function() {
var _vector = new THREE.Vector3,
projector = new THREE.Projector(),
handleMouseDown, handleMouseMove, handleMouseUp;
handleMouseDown = function(evt) {
var ray, intersections;
_vector.set(
(evt.clientX / window.innerWidth) * 2 - 1,
-(evt.clientY / window.innerHeight) * 2 + 1,
1
);
projector.unprojectVector(_vector, camera);
ray =
new THREE.Raycaster(
camera.position,
_vector.sub(camera.position).normalize()
);
intersections = ray.intersectObjects(blocks);
if (intersections.length > 0) {
selected_block = intersections[0].object;
_vector.set(0, 0, 0);
selected_block.setAngularFactor(_vector);
selected_block.setAngularVelocity(_vector);
selected_block.setLinearFactor(_vector);
selected_block.setLinearVelocity(_vector);
mouse_position.copy(intersections[0].point);
block_offset.subVectors(
selected_block.position, mouse_position
);
intersect_plane.position.y = mouse_position.y;
}
};
handleMouseMove = function(evt) {
var ray, intersection, i, scalar;
if (selected_block !== null) {
_vector.set(
(evt.clientX / window.innerWidth) * 2 - 1,
-(evt.clientY / window.innerHeight) * 2 + 1,
1
);
projector.unprojectVector(_vector, camera);
ray =
new THREE.Raycaster(
camera.position,
_vector.sub(camera.position).normalize()
);
intersection = ray.intersectObject(intersect_plane);
mouse_position.copy(intersection[0].point);
}
};
handleMouseUp = function(evt) {
if (selected_block !== null) {
_vector.set(1, 1, 1);
selected_block.setAngularFactor(_vector);
selected_block.setLinearFactor(_vector);
selected_block = null;
}
};
return function() {
var de = renderer.domElement;
de.addEventListener('mousedown', handleMouseDown);
de.addEventListener('mousemove', handleMouseMove);
de.addEventListener('mouseup', handleMouseUp);
};
})();
window.onload = initScene;
function refreshPage(args) {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
registerCallback("refresh", refreshPage);
</script>
</head>
<body>
<div id="viewport"></div>
<div id="heading">
<h1 onclick="sendBlocks();">
Have a play, then click here to import
</h1>
</div>
</body>
</html>
That’s it for this brief look at integrating physics into AutoCAD palettes, and having the HTML/JavaScript code hosted in those palettes drive AutoCAD model changes. The sample I chose lacks practical value in and of itself, but that’s not really the point. The point is to show that you can connect a physics-based 3D scene that’s hosted in an HTML palette with AutoCAD. In this case we’ve made the link in one direction but we might also have populated the scene with the contents of the modelspace. In any case, Balaji’s already done a nice real-world sample, as we saw earlier, which left plenty of space for me to fool around.
If you’re after a deeper physics integration, I suggest looking into the work done by my team-mate Christer Janson – which despite being mentioned in this April Fools’ post does actually exist – using ObjectARX to integrate the Box2D physics engine into AutoCAD to give the modelspace contents physical properties and behaviours. You can download his presentation with demo material from the AU website.