Skip directly to content

3D

Langton's Ant in 3d, Using Javascript and Three.js

on Fri, 03/02/2012 - 22:36

Langton's Ant in 3d, using Javascript and Three.js

Click the screenshot to launch the demo. You will need to use the latest version of Chrome, Firefox or Safari if you want to see it running.

This is a re-creation of a Flash experiment from several years ago. Back before I started playing around with 3d, I created a series of experiments exploring cellular automata, the majority of which were variations on the Langton's Ant algorithm. The most advanced version was one in pseudo-3d, and that was the last for over three years. Now with the advent of easy-to-use 3d libraries (like Three.js, used here), and the option of hardware acceleration to allow for complex animations, I felt it was time to re-visit some of the old experiments.

Here is the source code for the 3d ant:

window.requestAnimFrame = (function(){
	return  window.requestAnimationFrame	|| 
		window.webkitRequestAnimationFrame || 
		window.mozRequestAnimationFrame    || 
		window.oRequestAnimationFrame      || 
		window.msRequestAnimationFrame     || 
		function( callback ){
			window.setTimeout(callback, 1000 / 60);
		};
})();

var canvas,
	stage,
	container,
	scene,
	camera,
	renderer,
	projector,
	light;
	
var WIDTH = 640,
	HEIGHT = 640;
	VIEW_ANGLE = 45,
	ASPECT = WIDTH/HEIGHT,
	NEAR = .1,
	FAR = 10000,
	centerX = 0,
	centerY = 0,
	centerZ = 0,
	cameraX = 0,
	cameraY = 200,
	cameraZ = 400;
	
var antX = 32,
	antY = 32,
	antZ = 32,
	nextX,
	nextY,
	nextZ,
	cellsX = 64,
	cellsY = 64,
	cellsZ = 64,
	cellWidth = 8,
	cellHeight = 8,
	cellDepth = 8,
	antSize = 7,
	maxDirections = 8,
	colorMultiplier = Math.round(256/cellsX),
	xOff = cellsX/2*cellWidth,
	yOff = cellsY/2*cellHeight,
	zOff = cellsZ/2*cellDepth,
	base,
	objects,
	antDirection = 1,
	filledCells=0,
	
	isRotating=true,
	isPaused=false;
	
	function init() {
		canvas = document.getElementById("myCanvas");
		scene = new THREE.Scene();
		camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR );
		camera.position.x = cameraX;
		camera.position.y = cameraY;
		camera.position.z = cameraZ;
		camera.lookAt(scene.position);
		scene.add( camera );
		projector = new THREE.Projector();
		renderer = new THREE.WebGLRenderer( { antialias: true } );
		renderer.setSize( WIDTH, HEIGHT );
		light = new THREE.SpotLight();
		light.position.set( cameraX, cameraY, cameraZ );
		scene.add(light);
		canvas.appendChild( renderer.domElement );
		renderer.render(scene,camera);
		
		base = new THREE.Object3D();
		base.position.x = centerX;
		base.position.y = centerY;
		base.position.x = centerZ;
		base.rotation.y = 45;
		scene.add(base);
		camera.lookAt(base.position);
		objects = new Array(cellsX);
		for(var i=0;i<objects.length;i++) {
			objects[i] = new Array(cellsY);
			for(var j=0;j<objects[i].length;j++) {
				objects[i][j] = new Array(cellsZ);
			}
		}
		onEnterFrame();
	}
	
	function onEnterFrame() {
		requestAnimFrame(onEnterFrame);
		renderer.render(scene,camera);
		if(isPaused==true) return;
		if(!objects[antX][antY][antZ]) {
			antDirection++;
			if(antDirection == maxDirections) antDirection = 0;
			addObject(antX,antY,antZ);
		} else {
			removeObject(antX,antY,antZ);
			antDirection--;
			if(antDirection == -1) antDirection = maxDirections-1;
		}
		switch(antDirection) {
			case 0:
				antZ--;
				break;
			case 1:
				antX++;
				break;
			case 2:
				antY++;
				break;
			case 3:
				antX--;
				break;
			case 4:
				antZ++;
				break;
			case 5:
				antX++;
				break;
			case 6:
				antY--;
				break;
			case 7:
				antX--;
				break;
			default:
				break;
		}
		if(antY < 0) antY += cellsY;
		if(antY >= cellsY) antY -= cellsY;
		if(antX < 0) antX += cellsX;
		if(antX >= cellsX) antX -= cellsX;
		if(antZ < 0) antZ += cellsZ;
		if(antZ >= cellsZ) antZ -= cellsZ;
		if(isRotating==true) base.rotation.y +=.01;
		document.getElementById("count").value = filledCells;
	}
	function addObject(x,y,z) {
		var sphereMaterial = new THREE.MeshPhongMaterial({
			color: getRGB(x,y,z),
			opacity:1
		});	
		var obj = new THREE.Mesh(
		   new THREE.CubeGeometry(antSize,antSize,antSize),
		   sphereMaterial);
		obj.position.x = x*cellWidth-xOff;
		obj.position.y = y*cellHeight-yOff;
		obj.position.z = z*cellDepth-zOff;
		obj.overdraw = true;
		base.add(obj);
		objects[x][y][z] = obj;
		filledCells++;
	}
	function removeObject(x,y,z) {
		base.remove(objects[x][y][z]);
		objects[x][y][z] = null;
		filledCells--;
	}
	function getRGB(r,g,b) {
		var rgb = parseInt((r*colorMultiplier).toString(16) + (g*colorMultiplier).toString(16) + (b*colorMultiplier).toString(16),16);
		return rgb;
	}
	onload=init;

The above code assumes you have downloaded a copy of Three.js and have a HTML page with appropriate elements set up. To see the entirety of the code I used, right-click on the screenshot and open it in a new tab or window, then view source. It's all one file, other than the Three.js library. Feel free to copy it in its entirety and play around.

HTML5 Canvas Drawing Library Exploration: Three.js

on Wed, 02/15/2012 - 22:12

This is the sixth in an ongoing series of posts exploring various Javascript drawing libraries.

Three.js is a powerful 3d drawing library, and currently the one to beat when it comes to leveraging the latest browser capabilities. When available, it can tie in to the WebGL rendering engine (current browsers on fairly new computers) for an experience which is very close to what you would get from a native application. This demo is not that ambitious, but it should give you a basic idea of how to get started building interactive 3d applications in Javascript.

Click here to launch the demo. Give it a couple of moments to load; the Three.js library is over 300kb.

HTML

<!DOCTYPE html>
<html>
	<head>
		<title>Trigonometron built using Three.js</title>
		<style>
			* {margin:0;padding:0;}
			body {background:#ffffff;}
			#myCanvas {width:800px;height:480px;position:absolute;left:0;top:0;background:#ffffff;}
		</style>
		<script type="text/javascript" src="Three.js"></script>
	</head>
	<body>
		<div id="myCanvas"></div>
		<script type="text/javascript">
			//	Javascript goes here
		</script>
	</body>
</html>

Fairly basic stuff here. Note that the container for the app is a <div/> element, not a <canvas/> element. Now on to the good bits.

Javascript

// shim layer with setTimeout fallback set to 60 frames per second
window.requestAnimFrame = (function(){
	return  window.requestAnimationFrame	|| 
		window.webkitRequestAnimationFrame || 
		window.mozRequestAnimationFrame    || 
		window.oRequestAnimationFrame      || 
		window.msRequestAnimationFrame     || 
		function( callback ){
			window.setTimeout(callback, 1000 / 60);
		};
})();

var canvas,
	stage,
	container,
	scene,
	camera,
	renderer,
	projector,
	light,
	
	currentShape="circle",
	isPaused = true,
	isInitialized = false,
	currentHoverTarget = null;


	
var shapes = ["circle","tricuspoid","tetracuspoid","epicycloid","epicycloid 2","epicycloid 3","lissajous","lemniscate","butterfly"];
var w = 800;
var h = 480;
var VIEW_ANGLE = 45;
var ASPECT = w/h;
var NEAR = .1;
var FAR = 10000;
var centerX = -100;
var centerY = 0;
var centerZ = -150;
var radius_x = 175;
var radius_y = 175;
var radius_z = 175;
var theta = 0;
var objects = [];
var buttons = [];
var numObjects = 0;
var r2d = 180/Math.PI;
var d2r = Math.PI/180;
var orbitSteps = 240;
var orbitSpeed = Math.PI*2/orbitSteps;
var objectInterval;
var objectPosition;
var direction = 1;
var index = 0;
var xVar1 = 0;
var xVar2;
var xVar3;
var xVar4;
var startingObjects = 100;
var newX;
var newY;
var newZ = centerZ;


function init() {
	canvas = document.getElementById("myCanvas");
	scene = new THREE.Scene();
	camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR );
	camera.position.y = 0;
	camera.position.z = 500;
	scene.add( camera );
	projector = new THREE.Projector();
	renderer = new THREE.WebGLRenderer( { antialias: true } );
	renderer.setSize( w, h );
	light = new THREE.SpotLight();
	light.position.set( centerX, centerY, 160 );
	scene.add(light);
	canvas.appendChild( renderer.domElement );
	renderer.render(scene,camera);
	canvas.addEventListener( 'mousedown', onDocumentMouseDown, false );
	canvas.addEventListener( 'mousemove', onDocumentMouseMove, false );
	
	initInterface();
	initObjects();
	onEnterFrame();
	isInitialized = true;
}
onload = init;


function initInterface() {
	var xOff = 310;
	var yOff = 150;
	var zOff = -100;
	for(var i=0;i<shapes.length;i++) {
		buildTextButton(shapes[i],xOff,yOff,zOff,120,20,15);
		yOff -= 23;
	}
	yOff -= 23;
	buildTextButton("play/pause",xOff,yOff,zOff,120,20,15);

}
function buildTextButton(text,bX,bY,bZ,bW,bH,bD) {
	var materials = [];
	var co = 0xededed;
	for ( var j = 0; j < 6; j ++ ) {
		materials.push(new THREE.MeshLambertMaterial({color:co,opacity:1}));
	}
	var ctx = document.createElement('canvas');
	ctx.getContext('2d').font = '12px Courier,monospace';
	ctx.getContext('2d').fillStyle = '#000000';
	ctx.getContext('2d').textAlign="center";
	ctx.getContext('2d').fillText(text, 125,10);
	var tex = new THREE.Texture(ctx);
	tex.needsUpdate = true;
	var mat = new THREE.MeshBasicMaterial({map: tex});
	mat.transparent = true;
	var textRender = new THREE.Mesh(
		new THREE.PlaneGeometry(ctx.width, ctx.height),
		mat
	);
	textRender.doubleSided = true;
	textRender.position.x = bX+25;
	textRender.position.y = bY-70;
	textRender.position.z = bZ+8;
	scene.add(textRender);
	var c = new THREE.Mesh(
		new THREE.CubeGeometry(bW,bH,bD, 1, 1, 1,materials ),
		new THREE.MeshFaceMaterial()
	);

	c.position.x = bX;
	c.position.y = bY;
	c.position.z = bZ;
	c.overdraw = true;
	c.name = text;
	scene.add(c);
	buttons.push(c);
}
function initObjects() {
	for(var i=0;i<startingObjects;i++) {
		addObject();
	}
}
function addObject() {
	var sphereMaterial = new THREE.MeshLambertMaterial({
		color: Math.round(Math.random()*0xffffff)
	});	
	var obj = new THREE.Mesh(
	   new THREE.SphereGeometry(10,16,16),	//	radius,segments,rings
	   sphereMaterial);

	obj.position.x = centerX
	obj.position.y = centerY
	obj.position.z = centerZ;
	obj.overdraw = true;
	scene.add(obj);
	objects.push(obj);
	numObjects = objects.length;
	objectInterval = orbitSteps/numObjects;
}

function onEnterFrame() {
	requestAnimFrame(onEnterFrame);
	renderer.render(scene,camera);
	if(isPaused==true && isInitialized==true) return;
	for(var i = 0; i < numObjects; i++) {
		objectPosition = orbitSpeed*objectInterval*i;    //    each object is individually updated
		switch(currentShape) {
			case "circle":
				newX = centerX + radius_x * Math.cos(theta + objectPosition);
				newY = centerY + radius_y * Math.sin(theta + objectPosition);
				break;
			case "tricuspoid":
				newX = centerX + (radius_x*.5) * ((2 * Math.cos(theta + objectPosition)) + Math.cos(2 * (theta + objectPosition)));
				newY = centerY + (radius_y*.5) * ((2 * Math.sin(theta + objectPosition)) - Math.sin(2 * (theta + objectPosition)));
				break;
			case "tetracuspoid":
				newX = centerX + radius_x * Math.pow((Math.cos(theta + objectPosition)),3);
				newY = centerY + radius_y * Math.pow((Math.sin(theta + objectPosition)),3);
				break;
			case "epicycloid":
				newX = centerX + (radius_x*.4) * Math.cos(theta + objectPosition) - radius_x*1*(Math.cos((radius_x/radius_x + 1) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * Math.sin(theta + objectPosition) - radius_y*1*(Math.sin((radius_y/radius_y + 1) * (theta + objectPosition)));
				break;
			case "epicycloid 2":
				newX = centerX + (radius_x*.4) * Math.cos(theta + objectPosition) - radius_x*1*(Math.cos((radius_x/radius_x + 2) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * Math.sin(theta + objectPosition) - radius_y*1*(Math.sin((radius_y/radius_y + 2) * (theta + objectPosition)));
				break;
			case "epicycloid 3":
				newX = centerX + (radius_x*.4) * Math.cos(theta + objectPosition) - radius_x*1*(Math.cos((radius_x/radius_x + 3) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * Math.sin(theta + objectPosition) - radius_y*1*(Math.sin((radius_y/radius_y + 3) * (theta + objectPosition)));
				break;
			case "lissajous":
				newX = centerX + radius_x * (Math.sin(3 * (theta + objectPosition) + xVar1));
				newY = centerY + radius_y * Math.sin(theta + objectPosition);
				xVar1 += .0005;
				break;
			case "lemniscate":
				newX = centerX + (radius_x*1.2) * ((Math.cos(theta + objectPosition)/(1 + Math.pow(Math.sin(theta + objectPosition),2))));
				newY = centerY + (radius_y*1.2) * (Math.sin(theta + objectPosition) * (Math.cos(theta + objectPosition)/(1 + Math.pow(Math.sin(theta + objectPosition),2))));
				break;
			case "butterfly":
				newX = centerX + (radius_x*.4) * (Math.cos(theta + objectPosition) * (Math.pow(5,Math.cos(theta+objectPosition)) - 2 * Math.cos(4 * (theta+objectPosition)) - Math.pow(Math.sin((theta+objectPosition)/12),4)));
				newY = centerY + (radius_y*.4) * (Math.sin(theta + objectPosition) * (Math.pow(5,Math.cos(theta+objectPosition)) - 2 * Math.cos(4 * (theta+objectPosition)) - Math.pow(Math.sin((theta+objectPosition)/12),4)));
				break;
			default:
				break;
	
		}
		objects[i].position.x = newX;
		objects[i].position.y = newY;
	}
	theta += (orbitSpeed*direction);
}

function onDocumentMouseMove( event ) {
	var targetObj = getIntersectedObject(event);
	if(targetObj == null) {
		document.getElementById("myCanvas").style.cursor="default";
		if(currentHoverTarget != null) {
			setObjectColor(currentHoverTarget,0xededed);
			currentHoverTarget = null;
		}
	} else {
		document.getElementById("myCanvas").style.cursor="pointer";
		if(targetObj != currentHoverTarget && currentHoverTarget != null) {
			setObjectColor(currentHoverTarget,0xededed);
		}
		setObjectColor(targetObj,0xffffff);
		currentHoverTarget = targetObj;
	}
}

function setObjectColor(thing,c) {
	for(var i=0;i<6;i++) {
		thing.geometry.materials[i].color.setHex(c);
	}
}

function onDocumentMouseDown( event ) {
	var targetObj = getIntersectedObject(event);
	if(targetObj != null) {
		if(targetObj.name=="play/pause") {
			isPaused = !isPaused;
		} else {
			currentShape = targetObj.name;
		}
	}
}
function getIntersectedObject(event) {
	event.preventDefault();
	var mouseX, mouseY;
	if(event.offsetX) {
		mouseX = event.offsetX;
		mouseY = event.offsetY;
	}
	else if(event.layerX) {
		mouseX = event.layerX;
		mouseY = event.layerY;
	}
	var vector = new THREE.Vector3(
		(mouseX / w) * 2 - 1,
		-(mouseY / h) * 2 + 1,
		0.5
	);
	projector.unprojectVector( vector, camera );
	var ray = new THREE.Ray( camera.position, vector.subSelf( camera.position ).normalize() );
	var intersects = ray.intersectObjects( buttons );
	if ( intersects.length > 0 ) {
		return intersects[0].object;
	} else {
		return null;
	}
}

Here is a line-by-line breakdown of the code.

  • 1-11 - requestAnimFrame - this is a custom function which tries to take advantage of the built-in (for new browsers) requestAnimationFrame functionality, which is meant to be used for frame-based animations (like Adobe Flash uses). Each browser uses its own variations, so this function tries each one, and if it is in a browser which does not support the functionality, it defaults to a setTimeout call.
  • 13-25 - initialize global variables. The first eight - "canvas" to "light", are used by the Three.js to create, control, and render the 3d scene to the canvas.
  • 29-61 - initialize variables for the animation. These are not specific to Three.js. Note, however, that there are now "z" variables in addition to "x" and "y"
  • 64-86 - init() - set up 3d environment and add all of the graphic elements
    • 65 - associate the app with an HTML element
    • 66 - create a new scene for display
    • 67-70 - initialize and position a camera, relative to the scene. The camera is the "screen" through which you look at the scene
    • 71 - create a new projector
    • 72-73 - initialize the renderer, which controls how the app will be drawn to the screen. In tthe his instance, we will use the WebGL renderer
    • 74-76 - create a new light source for the scene
    • 77 - add the newly created renderer to the HTML DOM
    • 78 - draw the app to the screen
    • 79-80 - add mouse listeners
    • 82 - create the UI buttons
    • 83 - create the graphics for the animations
    • 84 - call the animation once to display everything to the screen
    • 85 - we are now initialized and ready to go
  • 87 - tell the browser to call the init() method as soon as the page finishes loading
  • 90-101 - initInterface() - create the UI elements for the app
    • 91-93 - set the initial x, y, and z positions for the UI buttons
    • 94-97 - create one text button for each element in the shapes[] array
    • 98 - add some space between the shape buttons and the play/pause button
    • 99 - create the play/pause button
  • 102-138 - buildTextButton() - create a 3d rectangle button, add text to one face, and position it on the screen
    • 103 - initialize the materials array
    • 104 - set the default color of the buttons
    • 105-107 - create a new Lambert Material for each face of the cube. Lambert materials can be lit by spotlights and cast shadows. Normal materials cannot.
    • 108-112 - create a new canvas element, add text to it, and style the text
    • 113-114 - copy the canvas element to a Texture
    • 115-116 - draw the texture to a Material
    • 117-125 - create a new display object, fill it with the Material, and position it in 3d space. In this case, it will have the same x and y coordinates as the gray box behind it, but will hover one "pixel" from the front face of the box. I am sure there are ways to draw the text directly to the face of a cube, but I have not yet discovered it. This is a hack.
    • 126-135 - create a cube display object, color it in with the materials array, and position it in 3d space
    • 136 - add the cube display object to the display list
    • 137 - add the cube to the array of buttons. This is for determining mouse interactions
  • 139-143 - initObjects() - create a number of graphic element for the animations, equal to the value of startingObjects
  • 144-160 - addObject() - create a new display object for the animations
    • 145-147 - create a new material and give it a random color
    • 148-150 - create a new Sphere display object
    • 152-154 - position the object in 3d space
    • 155 - set the material on top of the wireframe of the model, so the "edges" don't show through
    • 156 - add the Sphere to the display list
    • 157 - add a reference to the Sphere to the array of spheres. Used for animations
    • 158-159 - update global variables to correctly set spacing of objects within the animations
  • 162-214 - onEnterFrame() - update the animation by one step
    • 163 - advance everything by one frame
    • 164 - draw everything to the screen
    • 165 - if the app is finished initializing, but is paused, do nothing
    • 166-212 - cycle through each Sphere display object in the animation and update its position on the screen. Note that this app only updates the Spheres in the x and y planes; z remains constant.
    • 213 - update the progress of the animation as a whole
  • 216-232 - onDocumentMouseMove() - called whenever the mouse is moved in the app; used to determine which button is currently being moused over
    • 217 - call getIntersectedObject() to determan which (if any) of the buttons is currently being moused over
    • 218-223 - if there is nothing being moused over...
      • 219 - set the cursor to the default
      • 220-223 - if there was an element being moused over in the last frame iterations...
        • 221 - call setObjectColor() to return the color of the rectangle to its default
        • 222 - set the currentHoverTarget to null
    • 224-231 - else if one of the buttons IS being moused over...
      • 225 - set the cursor to a pointer
      • 226-228 - if the current hover target is not the same as the one hovered over in the previous frame, yet SOMETHING was hovered over in the previous frame, set the previous target's color to the default
      • 229 - set the color of the current target to the hover highlight color
      • 230 - set the global currentHoverTarget to equal the button which is currently being hovered over
  • 234-238 - setObjectColor() - cycle through the six faces of a button and set each face to the appropriate color
  • 240-249 - onDocumentMouseDown() - called whenever the mouse button is pressed within the app
    • 241 - find out which button, if any, was clicked
    • 242-248 - if a button was clicked on...
      • 243-244 - if it was the play/pause button, toggle the pause variable
      • 245-247 - if it was any other button, set the currentShape variable to the sppropriate value
  • 250-275 - getIntersectedObject() - determine if a mouse event happened where it might intersect a target display object
    • 251 - stop the mouse event from cascading through the rest of the DOM
    • 252 - declare coordinate variables
    • 253-260 - based on how the browser handles mouse events, determine where on the app the event occurred
    • 261-265 - create a new vector object based on the x, y, and z coordinates of the event within the app
    • 266-267 - calculate where the objects in 3d space intersect the screen (the camera) in 2d space
    • 268 - build an array of every object in the buttons[] array which intersects the mouse event
    • 269-270 - if there is more than 0 elements in this array, return the first (top) display object
    • 271-273 - if nothing is intersected, do nothing

So there you have it: The trigonometron in 3d. Well, 3d-ish. It exists in 3d space, but everything is on the same plane, so I suppose it is de-facto 2d. I have a couple of 3d demos which I will post in the next few days. All in all, figuring out the 3d stuff was less difficult than I thought it would be. My experiments last fall with Away3d and Flash certainly helped.

This will be the last library exploration for a while. Further posts will must likely be experiments using Three.js, or one of the other libraries I explored in this series of posts.

In case you missed them the previous posts in the series are here:

Away3d and Flash Player 11

on Thu, 11/03/2011 - 22:25

Butterfly Curve animated with Away3d

Click the image above to launch the experiment. Requires Flash Player 11. May beat up on older computers. A lot.

What you see here is a simple example of what the Flash 11 player is capable of. There are 500 cubes, dynamically lit, moving through a "Transcendent Butterfly" curve on the x and y axes, and a variation of an epicycloid in the Z. The whole formation is oscillating through the x, y, and z axes as well. The little box in the upper left corner shows frame rate and the amount of RAM which the animation is using. I have had as many as 1000 cubes running through this animation but the frame rate dropped down below 30 FPS. 500 cubes is plenty for the moment.

This animation was created using the Away3d code library.

Another PhotoFly Test

on Sun, 10/09/2011 - 09:23

Here is another PhotoFly test. This is a stone bench outside of GRCC. PhotoFly found enough texture in the concrete that it was able to create the scene in one go, without me having to manually designate common points in any of the photos. You can see where PhotoFly still has problems with some areas, particularly where the texture in the foreground is too similar to the texture in the background. Notice the distortion on the bottom of the right underside of the bench. Also in the close-up of the underside there are a couple of small holes.

One improvement for PhotoFly would be the option to go back and fix errors which it has made when stitching photos. Alternately, re-render the scene, perhaps having the rendering engine run through the photos in a different order so it comes up with different "assumptions" about how the points in the photos fit together.

Bench 13

Also, here is one of the photos I used to create this render. Click it to see the rest. There are 23, and they are all of the bench. Not terribly exciting, but you will get an idea of how PhotoFly pulls information to create a 3d object.

More Thoughts About PhotoFly

on Tue, 10/04/2011 - 08:10

Off and on over the past several weeks I have wandered around town with my camera looking for likely subjects to turn into 3d digital representations of themselves. My success rate is about .5, and is mostly made up of tree trunks and patches of gravel. Small objects, and objects in a light box, have not worked at all. I don't know if this is a fundamental flaw with PhotoFly, an artifact of PhotoFly being in beta, or if I just don't get it. I suspect (and hope) it is a mix of the latter two.

But enough of that. Of my successes, I have created animations of the best ones and posted them on YouTube.

 

This is the first animation I created. PhotoFly makes this quite easy, with a well-thought-out timeline-based animation tool. The gaps in the scene are places where the camera could not see the environment from where I took the photos. While beautiful, there are not a lot of vantage points at the koi pond.

 

This tree trunk is the second successful scene. The photos are from my parent's house in Springport. I believe I took around 20 photos. Notice the gaps in the grass around the highest-resolution parts of the lawn. This is where PhotoFly couldn't quite figure out how to stitch parts of the scene together, because grass is too uniform a color and texture for the software to sort out.

 

This one is my favorite so far. the overpass is a block from my house. I was wandering around with my camera when I noticed an extraordinary piece of graffiti on the concrete embankment. I took a few photos, then began wandering up and down the tracks, and up into the nooks and crannies of the overpass, trying to get everything from every angle. Mostly, I succeeded. The bridge is quite new, and nowhere near as post-apocalyptic in real life as it appears in the animation. This is my only successful attempts at modelling a hollow structure.

I went back a couple of weeks later, intending to model the entire overpass, including the railroad track leading into it. Unfortunately, the regularity, the sameness of the man-made parts of the scene confounded PhotoFly, and of the hundred or so photos I took, PhotoFly only managed to incorporate about 20 into the final scene, which looked like someone had printed a photo of the bridge onto a wad of silly putty, then twisted it up and thrown it against a wall. I suspect that a more judicious use of angles when taking photos would make a future attempt more successful.

 

In my opinion, this is the most successful of all of my PhotoFly experiments, simply because this is the one with the least amount of distortion. The photos which went into this scene are from the Lake Michigan shoreline, just north of Oval Beach in Saugatuck, Michigan. There was enough light, and enough varied texture, that the software created this scene in one go. I didn't need to define any points or re-stitch any of the photos. It just worked.

 

This is the most recent one. A goose-neck gourd, on a foot stool in my back yard. I would call it a qualified success. The yard looks great! The gourd, other than the neck, looks pretty good. The footstool - the man-made, smooth, texture-less object - is warped and distorted, and has been melded with the background. This one probably suffered a little from the bright sunlight. The gourd is smooth and shiny, and some of its color patterns were obscured by reflections.

The three things PhotoFly seems to have the most difficulty with are reflections, lack of context, and sharply contrasting light sources. The pattern recognition part of PhotoFly can't (at present) distinguish between a pattern and a reflection of a pattern. This makes sense; it tries to find and reproduce patterns. If two parts of a photo have the same pattern, it is difficult to decide which part goes where, without a lot of other contextual information.

Which is why PhotoFly doesn't work well with, for instance, something in a light box. The thing itself may have astonishing detail, but without detailed surroundings to give it a location in space, PhotoFly can't (again, at present) determine angles, curves, relative distances, and the like. This is one case where having a light source which is the same strength, everywhere at once, is actually a detriment.

With brightly contrasting light, say, a plain-colored object in full afternoon sunlight, PhotoFly doesn't necessarily recognize that the shady side of an object is attached to the sunny side of the object. If the object has a rich texture, lots of additional information which the software can use to create context, this is not such a problem, but a photo of e.g. a large rock, partially silhouetted against the sky, doesn't work so well.

Having figured these issues out, it is simple to come up with succcessful PhotoFly scenes. If I discover a workaround to any of the above issues, I will post it here and at the AutoDesk Labs forums.

Pages