Demo is available here:

Code is available here:
http://code.google.com/p/hangout-experiments/source/browse/#git%2Fface
Outside of showing off some relatively straightforward three.js, the app also demonstrates the value of smoothing. The app keeps a 5-frame window of values for tilt/pan/roll and then takes a weighted average of them to choose the face position for that frame. Key source snippets are here:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Computes weighted array | |
function computeWeightedWindow(array) { | |
var len = array.length; | |
var totalCount = 0; | |
var sumSoFar = 0; | |
for (var i = 0; i < len; i++) { | |
totalCount += (len - i); | |
sumSoFar += array[i] * (len - i); | |
} | |
return sumSoFar / totalCount; | |
} | |
// Render call; called 30 fps | |
function render() { | |
plane.rotation.y = group.rotation.y = -computeWeightedWindow(lastPan); | |
group.rotation.x = -computeWeightedWindow(lastTilt); | |
group.rotation.z = computeWeightedWindow(lastRoll); | |
group.position.z = 1000 * computeWeightedWindow(lastScale); | |
plane.position.z = 1000 * computeWeightedWindow(lastScale); | |
group.position.x = -500 * computeWeightedWindow(lastX); | |
plane.position.x = -500 * computeWeightedWindow(lastX); | |
renderer.render(scene, camera); | |
} | |
// Called in response to changes in the face location. | |
function onFaceTrackingDataChanged(event) { | |
lastEvent = event; | |
if (!event.hasFace) { | |
return; | |
} | |
lastRoll.unshift(event.roll); | |
lastRoll.pop(); | |
lastPan.unshift(event.pan); | |
lastPan.pop(); | |
lastTilt.unshift(event.tilt); | |
lastTilt.pop(); | |
// etc... | |
} |