Browse Source

Added 3DSMax skin and morph export.

Charles Lewis 12 years ago
parent
commit
8f804665fa

+ 128 - 0
examples/js/UCSCharacter.js

@@ -0,0 +1,128 @@
+THREE.UCSCharacter = function() {
+
+	var scope = this;
+	
+	var mesh;
+
+	this.scale = 1;
+
+	this.root = new THREE.Object3D();
+	
+	this.numSkins;
+	this.numMorphs;
+	
+	this.skins = [];
+	this.materials = [];
+	this.morphs = [];
+
+	this.onLoadComplete = function () {};
+	
+	this.loadCounter = 0;
+
+	this.loadParts = function ( config ) {
+		
+		this.numSkins = config.skins.length;
+		this.numMorphs = config.morphs.length;
+		
+		// Character geometry + number of skins
+		this.loadCounter = 1 + config.skins.length;
+		
+		// SKINS
+		this.skins = loadTextures( config.baseUrl + "skins/", config.skins );
+		this.materials = createMaterials( this.skins );
+		
+		// MORPHS
+		this.morphs = config.morphs;
+		
+		// CHARACTER
+		var loader = new THREE.JSONLoader();
+		console.log( config.baseUrl + config.character );
+		loader.load( config.baseUrl + config.character, function( geometry ) {
+			geometry.computeBoundingBox();
+			geometry.computeVertexNormals();
+
+			THREE.AnimationHandler.add( geometry.animation );
+
+			mesh = new THREE.SkinnedMesh( geometry, new THREE.MeshFaceMaterial() );
+			scope.root.add( mesh );
+			
+			var bb = geometry.boundingBox;
+			scope.root.scale.set( config.s, config.s, config.s );
+			scope.root.position.set( config.x, config.y - bb.min.y * config.s, config.z );
+
+			mesh.castShadow = true;
+			mesh.receiveShadow = true;
+
+			animation = new THREE.Animation( mesh, geometry.animation.name );
+			animation.JITCompile = false;
+			animation.interpolationType = THREE.AnimationHandler.LINEAR;
+
+			animation.play();
+			
+			scope.setSkin(0);
+			
+			scope.checkLoadComplete();
+		} );
+
+	};
+	
+	this.setSkin = function( index ) {
+		if ( mesh && scope.materials ) {
+			mesh.material = scope.materials[ index ];
+		}
+	};
+	
+	this.updateMorphs = function( influences ) {
+		if ( mesh ) {
+			for ( var i = 0; i < scope.numMorphs; i ++ ) {
+				mesh.morphTargetInfluences[ i ] = influences[ scope.morphs[ i ] ] / 100;
+			}
+		}
+	}
+	
+	function loadTextures( baseUrl, textureUrls ) {
+		var mapping = new THREE.UVMapping();
+		var textures = [];
+
+		for ( var i = 0; i < textureUrls.length; i ++ ) {
+
+			textures[ i ] = THREE.ImageUtils.loadTexture( baseUrl + textureUrls[ i ], mapping, scope.checkLoadComplete );
+			textures[ i ].name = textureUrls[ i ];
+
+		}
+
+		return textures;
+	};
+
+	function createMaterials( skins ) {
+		var materials = [];
+		
+		for ( var i = 0; i < skins.length; i ++ ) {
+			materials[i] = new THREE.MeshLambertMaterial( {"colorDiffuse"  : [0.5880, 0.5880, 0.5880],
+														  "colorAmbient"  : [0.5880, 0.5880, 0.5880],
+														  "colorSpecular"  : [0.5000, 0.5000, 0.5000],
+														  "color" : 0xeeeeee,
+														  "transparency"  : 1.0,
+														  "specularCoef"  : 10.0,
+														  "wireframe" : false,
+														  "vertexColors" : false,
+														  "map" : skins[i]} );
+			materials[i].skinning = true;
+			materials[i].morphTargets = true;
+
+			materials[i].wrapAround = true;
+			materials[i].perPixel = true;
+		}
+		
+		return materials;
+	}
+
+	this.checkLoadComplete = function () {
+		scope.loadCounter -= 1;
+		if ( scope.loadCounter === 0 ) {
+			scope.onLoadComplete();
+		}
+	}
+
+}
+

File diff suppressed because it is too large
+ 1 - 0
examples/js/libs/jquery-1.7.1.min.js


BIN
examples/models/skinned/UCS/skins/Asian_Male.jpg


BIN
examples/models/skinned/UCS/skins/Black_Female.jpg


BIN
examples/models/skinned/UCS/skins/Caucasion_Female.jpg


BIN
examples/models/skinned/UCS/skins/Caucasion_Male.jpg


BIN
examples/models/skinned/UCS/skins/Highlighted_Muscles.jpg


BIN
examples/models/skinned/UCS/skins/Indian_Male.jpg


BIN
examples/models/skinned/UCS/skins/Thumbs.db


File diff suppressed because it is too large
+ 30 - 0
examples/models/skinned/UCS/umich_ucs.js


+ 10 - 0
examples/models/skinned/UCS_config.json

@@ -0,0 +1,10 @@
+{
+	"baseUrl":		"models/skinned/UCS/",
+	"character":	"umich_ucs.js",
+	"skins": 		["Asian_Male.jpg", "Black_Female.jpg", "Caucasion_Female.jpg", "Caucasion_Male.jpg", "Highlighted_Muscles.jpg", "Indian_Male.jpg"],
+	"morphs": 		["Obesity", "Femininity", "Musculature", "Age", "Skinniness"],
+	"x":	0,
+	"y":	-500,
+	"z":	-300,
+	"s":	30
+}

+ 285 - 0
examples/webgl_max_skin_morph.html

@@ -0,0 +1,285 @@
+<!doctype html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - skinning + morphing [knight]</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<style>
+			body {
+				color: #000;
+				font-family:Monospace;
+				font-size:13px;
+				text-align:center;
+
+				background-color: #fff;
+				margin: 0px;
+				overflow: hidden;
+			}
+
+			#info {
+				position: absolute;
+				top: 0px; width: 100%;
+				padding: 5px;
+			}
+
+			a {
+				color: #0af;
+			}
+		</style>
+	</head>
+
+	<body>
+		
+		<div id="container"></div>
+
+		<div id="info">
+		<a href="http://github.com/mrdoob/three.js" target="_blank">three.js</a> webgl - skinning + morphing
+		- knight by <a href="http://vimeo.com/36113323">apendua</a>
+		</div>
+
+		<script src="../build/three.min.js"></script>
+		
+		<script src="js/UCSCharacter.js"></script>
+
+		<script src="js/Detector.js"></script>
+		<script src="js/libs/stats.min.js"></script>
+		
+		<script src='js/libs/dat.gui.min.js'></script>
+		
+		<script src="js/libs/jquery-1.7.1.min.js"></script>
+		
+		<script src="js/controls/OrbitControls.js"></script>
+		
+		<script>
+			
+			var SCREEN_WIDTH = window.innerWidth;
+			var SCREEN_HEIGHT = window.innerHeight;
+
+			var container,stats;
+
+			var camera, scene;
+			var renderer;
+			
+			var mesh;
+
+			var mouseX = 0, mouseY = 0;
+
+			var windowHalfX = window.innerWidth / 2;
+			var windowHalfY = window.innerHeight / 2;
+
+			var clock = new THREE.Clock();
+			
+			var gui, skinConfig, morphConfig;
+				
+			document.addEventListener( 'mousemove', onDocumentMouseMove, false );
+
+			init();
+			animate();
+
+			function init() {
+
+				container = document.getElementById( 'container' );
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 100000 );
+				camera.position.z = 10000;
+				camera.position.x = 5000;
+				camera.position.y = 10000;
+
+				scene = new THREE.Scene();
+
+				scene.fog = new THREE.Fog( 0xffffff, 20000, 100000 );
+				scene.fog.color.setHSL( 0.6, 0, 1 );
+
+				scene.add( camera );
+
+				// LIGHTS
+
+				var ambient = new THREE.AmbientLight( 0x222222 );
+				scene.add( ambient );
+
+
+				var light = new THREE.DirectionalLight( 0xffffff, 1.6 );
+				light.position.set( 0, 140, 500 );
+				light.position.multiplyScalar( 1.1 );
+				light.color.setHSL( 0.6, 0.075, 1 );
+				scene.add( light );
+
+				light.castShadow = true;
+
+				light.shadowMapWidth = 2048;
+				light.shadowMapHeight = 2048;
+
+				var d = 390;
+
+				light.shadowCameraLeft = -d * 2;
+				light.shadowCameraRight = d * 2;
+				light.shadowCameraTop = d * 1.5;
+				light.shadowCameraBottom = -d;
+
+				light.shadowCameraFar = 3500;
+				//light.shadowCameraVisible = true;
+
+				//
+
+				var light = new THREE.DirectionalLight( 0xffffff, 1 );
+				light.position.set( 0, -1, 0 );
+				light.color.setHSL( 0.25, 0.85, 0.5 );
+				scene.add( light );
+
+				// RENDERER
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setSize( SCREEN_WIDTH, SCREEN_HEIGHT );
+				renderer.domElement.style.position = "relative";
+
+				renderer.setClearColor( scene.fog.color, 1 );
+
+				container.appendChild( renderer.domElement );
+
+				renderer.gammaInput = true;
+				renderer.gammaOutput = true;
+				renderer.physicallyBasedShading = true;
+
+				renderer.shadowMapEnabled = true;
+
+				// STATS
+
+				stats = new Stats();
+				stats.domElement.style.position = 'absolute';
+				stats.domElement.style.top = '0px';
+				stats.domElement.style.zIndex = 100;
+				container.appendChild( stats.domElement );
+
+				stats.domElement.children[ 0 ].children[ 0 ].style.color = "#777";
+				stats.domElement.children[ 0 ].style.background = "transparent";
+				stats.domElement.children[ 0 ].children[ 1 ].style.display = "none";
+
+				// CHARACTER
+
+				character = new THREE.UCSCharacter();
+				character.onLoadComplete = function() {
+					console.log( "Load Complete" );
+					console.log( character.numSkins + " skins and " + character.numMorphs + " morphtargets loaded." );
+					gui = new dat.GUI();
+					setupSkinsGUI();
+					setupMorphsGUI();
+					gui.width = 400;
+					gui.open();
+				}
+				
+				$.getJSON("models/skinned/UCS_config.json", function( config ) {
+						character.loadParts( config );
+						scene.add( character.root );
+					}
+				);
+
+				window.addEventListener( 'resize', onWindowResize, false );
+				
+				controls = new THREE.OrbitControls( camera, renderer.domElement );
+				controls.center = new THREE.Vector3(0,3000,0);
+
+				controls.addEventListener( 'change', render );
+
+			}
+			
+			function setupSkinsGUI() {
+			
+				var skinGui = gui.addFolder( "Skins" );
+				
+				skinConfig = {
+					wireframe: false
+				};
+				
+				var skinCallback = function( index ) {
+					return function () {
+						character.setSkin( index );
+					};
+				}
+
+				for ( var i = 0; i < character.numSkins; i++ ) {
+					var name = character.skins[ i ].name;
+					skinConfig[ name ] = skinCallback( i );
+				}
+				
+				for ( var i = 0; i < character.numSkins; i++ ) {
+					skinGui.add( skinConfig, character.skins[i].name );
+				}
+				
+				skinGui.open();
+
+			}
+			
+			function setupMorphsGUI() {
+				
+				var morphGui = gui.addFolder( "Morphs" );
+				
+				morphConfig = {
+				};
+				
+				var morphCallback = function( index ) {
+					return function () {
+						character.updateMorphs( morphConfig );
+					}
+				}
+				
+				for ( var i = 0; i < character.numMorphs; i ++ ) {
+					var morphName = character.morphs[ i ];
+					morphConfig[ morphName ] = 0;
+				}
+				
+				for ( var i = 0; i < character.numMorphs; i ++ ) {
+					morphGui.add( morphConfig, character.morphs[ i ] ).min( 0 ).max( 100 ).onChange( morphCallback( i ) );
+				}
+				
+				morphGui.open();
+			
+			}
+
+			function onWindowResize() {
+
+				windowHalfX = window.innerWidth / 2;
+				windowHalfY = window.innerHeight / 2;
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function onDocumentMouseMove( event ) {
+
+				mouseX = ( event.clientX - windowHalfX ) * 10;
+				mouseY = ( event.clientY - windowHalfY ) * 10;
+
+			}
+
+			//
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				controls.update();
+
+				render();
+				stats.update();
+
+			}
+
+			function render() {
+
+				var delta = 0.75 * clock.getDelta();
+
+				// update skinning
+
+				THREE.AnimationHandler.update( delta );
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 1733 - 0
utils/exporters/max/ThreeJSAnimationExporter.ms

@@ -0,0 +1,1733 @@
+rollout ThreeJSExporter "ThreeJSExporter"
+(
+	-- Variables
+
+	local ostream,
+	
+	threeMatrix = (matrix3 [1,0,0] [0,0,1] [0,-1,0] [0,0,0]),
+
+	headerFormat = "\"metadata\":
+{
+\"sourceFile\": \"%\",
+\"generatedBy\": \"3ds max ThreeJSExporter\",
+\"formatVersion\": 3,
+\"vertices\": %,
+\"normals\": %,
+\"colors\": %,
+\"uvs\": %,
+\"triangles\": %,
+\"materials\": %
+},
+
+",
+
+	vertexFormat = "%,%,%",
+
+	vertexNormalFormat = "%,%,%",
+	UVFormat = "%,%",
+
+	triFormat = "%,%,%,%",
+	triUVFormat = "%,%,%,%,%,%,%",
+	triNFormat = "%,%,%,%,%,%,%",
+	triUVNFormat = "%,%,%,%,%,%,%,%,%,%",
+
+	footerFormat = "\n}",
+
+
+	
+	boneFormat = "\t\t{
+\t\t\t\"parent\" : %,
+\t\t\t\"name\"   : \"%\",
+\t\t\t\"pos\"    : %,
+\t\t\t\"scl\"    : %,
+\t\t\t\"rotq\"   : [%,%,%,%]
+\t\t}",
+
+	animHeaderFormat = "\t\"animation\" : {
+\t\t\"name\"      : \"Action\",
+\t\t\"fps\"       : %,
+\t\t\"length\" : %,
+\t\t\"hierarchy\" : [\n",
+	
+	animBoneHeaderFormat = "\t\t\t{
+\t\t\t\t\"parent\" : %,
+\t\t\t\t\"keys\"    : [\n",
+
+	keyFormat = "\t\t\t\t\t{
+\t\t\t\t\t\t\"time\":%,
+\t\t\t\t\t\t\"pos\" :[%,%,%],
+\t\t\t\t\t\t\"rot\" :[%,%,%,%],
+\t\t\t\t\t\t\"scl\" :%
+\t\t\t\t\t}",
+
+	animBoneFooterFormat = "\t\t\t\t]
+\t\t\t}",
+
+	animFooterFormat = "\n\n\t\t]
+\t}\n"
+
+
+	-------------------------------------------------------------------------------------
+	-- User interface
+
+
+	group "ThreeJSExporter  v0.8"
+	(
+
+		label msg "Exports selected meshes in Three.js ascii JSON format" align:#left
+		hyperLink lab1 "Original source at GitHub" address:"https://github.com/alteredq/three.js/blob/master/utils/exporters/max/ThreeJSExporter.ms" color:(color 255 120 0) align:#left
+
+		label dummy1 "--------------------------------------------------------" align:#left
+
+		checkbox exportColor "Export vertex colors" checked:false enabled:true
+		checkbox exportUv "Export uvs" checked:true enabled:true
+		checkbox exportNormal "Export normals" checked:true enabled:true
+		checkbox smoothNormal "Use vertex normals" checked:false enabled:true
+
+		label dummy2 "--------------------------------------------------------" align:#left
+
+		checkbox flipYZ "Flip YZ" checked:false enabled:false
+		checkbox flipUV "Flip UV" checked:false enabled:false
+		checkbox flipFace "Flip all faces" checked:false enabled:false
+		checkbox autoflipFace "Try fixing flipped faces" checked:false enabled:false
+		
+		label dummy3 "--------------------------------------------------------" align:#left
+		
+		spinner fps "Animation speed (FPS)" range:[0,1000,25] type:#integer
+
+		label dummy4 "--------------------------------------------------------" align:#left
+
+		button btn_export "Export selected objects"
+
+	)
+	
+	
+
+	-------------------------------------------------------------------------------------
+	-- Dump vertices
+
+	function DumpVertices src =
+	(
+
+		Format "\"vertices\": [" to:ostream
+
+		num = src.count
+
+		if num > 0 then
+		(
+
+			for i = 1 to num do
+			(
+
+				vert = src[i]
+
+				if flipYZ.checked then
+				(
+					x = vert.x
+					y = vert.z
+					z = vert.y
+
+					z *= -1
+
+				)
+				else
+				(
+
+					x = vert.x
+					y = vert.y
+					z = vert.z
+
+				)
+
+				Format vertexFormat x y z to:ostream
+
+				if i < num then Format "," to:ostream
+
+			)
+
+		)
+
+		Format "],\n\n" to:ostream
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Dump colors
+
+	function DumpColors src useColors =
+	(
+
+		Format "\"colors\": [" to:ostream
+
+		num = src.count
+
+		if num > 0 and useColors then
+		(
+
+			for i = 1 to num do
+			(
+
+				col = src[i]
+
+				r = col.r as Integer
+				g = col.g as Integer
+				b = col.b as Integer
+
+				hexNum = ( bit.shift r 16 ) + ( bit.shift g 8 ) + b
+
+				-- hexColor = formattedPrint hexNum format:"#x"
+				-- Format "%" hexColor to:ostream
+
+				decColor = formattedPrint hexNum format:"#d"
+				Format "%" decColor to:ostream
+
+				if i < num then Format "," to:ostream
+
+			)
+
+		)
+
+		Format "],\n\n" to:ostream
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Dump normals
+
+	function DumpNormals src =
+	(
+
+		Format "\"normals\": [" to:ostream
+
+		num = src.count
+
+		if num > 0 and exportNormal.checked then
+		(
+
+			for i = 1 to num do
+			(
+
+				normal = src[i]
+				normal = normalize normal as point3
+
+				if flipYZ.checked then
+				(
+
+					x = normal.x
+					y = normal.z
+					z = normal.y
+
+					z *= -1
+
+				)
+				else
+				(
+
+					x = normal.x
+					y = normal.y
+					z = normal.z
+
+				)
+
+				Format vertexNormalFormat x y z to:ostream
+
+				if i < num then Format "," to:ostream
+
+			)
+
+		)
+
+		Format "],\n\n" to:ostream
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Dump uvs
+
+	function DumpUvs src =
+	(
+
+		Format "\"uvs\": [[" to:ostream
+
+		num = src.count
+
+		if num > 0 and exportUv.checked then
+		(
+
+			for i = 1 to num do
+			(
+
+				uvw = src[i]
+
+				u = uvw.x
+
+				if flipUV.checked then
+				(
+					v = 1 - uvw.y
+				)
+				else
+				(
+					v = uvw.y
+				)
+
+				Format UVFormat u v to:ostream
+
+				if i < num then Format "," to:ostream
+
+			)
+
+		)
+
+		Format "]],\n\n" to:ostream
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Dump faces
+
+	function DumpFaces src useColors =
+	(
+
+		Format "\"faces\": [" to:ostream
+
+		num = src.count
+
+		if num > 0 then
+		(
+
+			for i = 1 to num do
+			(
+
+				zface = src[i]
+
+				fv  = zface[1]
+				fuv = zface[2]
+				m   = zface[3] - 1
+				fc  = zface[4]
+
+				needsFlip = zface[5]
+
+				isTriangle = true
+				hasMaterial = true
+				hasFaceUvs = false
+				hasFaceVertexUvs = ((classof fuv == Point3) and exportUv.checked)
+				hasFaceNormals = false
+				hasFaceVertexNormals = (exportNormal.checked)
+				hasFaceColors = false
+				hasFaceVertexColors = ((classof fc == Point3) and useColors)
+
+				faceType = 0
+				faceType = bit.set faceType 1 (not isTriangle)
+				faceType = bit.set faceType 2 hasMaterial
+				faceType = bit.set faceType 3 hasFaceUvs
+				faceType = bit.set faceType 4 hasFaceVertexUvs
+				faceType = bit.set faceType 5 hasFaceNormals
+				faceType = bit.set faceType 6 hasFaceVertexNormals
+				faceType = bit.set faceType 7 hasFaceColors
+				faceType = bit.set faceType 8 hasFaceVertexColors
+
+				if i > 1 then
+				(
+					Format "," faceType to:ostream
+				)
+
+				Format "%" faceType to:ostream
+
+				if isTriangle then
+				(
+
+					va = (fv.x - 1) as Integer
+					vb = (fv.y - 1) as Integer
+					vc = (fv.z - 1) as Integer
+
+					if flipFace.checked or needsFlip then
+					(
+
+						tmp = vb
+						vb = vc
+						vc = tmp
+
+					)
+
+
+					Format ",%,%,%" va vb vc to:ostream
+
+
+					if hasMaterial then
+					(
+
+						Format ",%" m to:ostream
+
+					)
+
+					if hasFaceVertexUvs then
+					(
+
+						ua = (fuv.x - 1) as Integer
+						ub = (fuv.y - 1) as Integer
+						uc = (fuv.z - 1) as Integer
+
+						if flipFace.checked or needsFlip then
+						(
+
+							tmp = ub
+							ub = uc
+							uc = tmp
+
+						)
+
+						Format ",%,%,%" ua ub uc to:ostream
+
+					)
+
+					if hasFaceVertexNormals then
+					(
+
+						if smoothNormal.checked then
+						(
+
+							-- normals have the same indices as vertices
+
+							na = va
+							nb = vb
+							nc = vc
+
+						)
+						else
+						(
+							-- normals have the same indices as face
+
+							na = i - 1
+							nb = na
+							nc = na
+
+						)
+
+						if flipFace.checked or needsFlip then
+						(
+
+							tmp = nb
+							nb = nc
+							nc = tmp
+
+						)
+
+						Format ",%,%,%" na nb nc to:ostream
+
+					)
+
+
+					if hasFaceVertexColors then
+					(
+
+						ca = (fc.x - 1) as Integer
+						cb = (fc.y - 1) as Integer
+						cc = (fc.z - 1) as Integer
+
+						if flipFace.checked or needsFlip then
+						(
+
+							tmp = cb
+							cb = cc
+							cc = tmp
+
+						)
+
+						Format ",%,%,%" ca cb cc to:ostream
+
+					)
+
+				)
+
+			)
+
+		)
+
+		Format "]" to:ostream
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Dump color
+
+	function DumpColor pcolor label =
+	(
+		r = pcolor.r / 255
+		g = pcolor.g / 255
+		b = pcolor.b / 255
+
+		fr = formattedPrint r format:".4f"
+		fg = formattedPrint g format:".4f"
+		fb = formattedPrint b format:".4f"
+
+		Format "\"%\"  : [%, %, %],\n" label fr fg fb to:ostream
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Dump map
+
+	function DumpMap pmap label =
+	(
+
+		if classof pmap == BitmapTexture then
+		(
+			bm = pmap.bitmap
+
+			if bm != undefined then
+			(
+
+				fname = filenameFromPath bm.filename
+				Format "\"%\"    : \"skins/%\",\n" label fname to:ostream
+
+			)
+
+		)
+
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Dump bones
+	-- src = #( #(index, name, position, scale, rotation), .. )
+	-- boneOrder lists the correct output order of the bones
+	-- newIndices is inverse of boneOrder
+	
+	function DumpBones src boneOrder newIndices =
+	(
+		numBones = boneOrder.count
+		
+		Format ",\n\n\t\"bones\" : [\n" to:ostream
+		
+		for i = 1 to numBones do
+		(
+			b = src[boneOrder[i]]
+			if b[1] == -1 then
+			(
+				parent_index = -1
+			) else (
+				parent_index = newIndices[b[1]+1]
+			)
+			bone_name = b[2]
+			p = b[3]
+			s = b[4]
+			r = b[5]
+			
+			Format boneFormat parent_index bone_name p s r.x r.y r.z r.w to:ostream
+			
+			if (i < numBones) then (Format "," to:ostream)
+			Format "\n\n" to:ostream
+		)
+		Format "\t],\n\n" to:ostream
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Dump skin indices
+	-- src = #( #(skinned?, #(index1A, index1B, ..), name )
+	-- If the mesh wasn't skinned, look in boneNames for its parent to fix the index
+	-- If it's not there, leave as 0
+	-- boneOrder lists the correct output order of the bones
+	-- newIndices is inverse of boneOrder
+	
+	function DumpIndices src boneNames newIndices =
+	(
+		output = #()
+		for i=1 to src.count do
+		(
+			if src[i][1] then
+			(
+				join output src[i][2]
+			) else (
+				bone = findItem boneNames src[i][3]
+				for j=1 to src[i][2].count do
+				(
+					src[i][2][j] = bone
+				)
+				join output src[i][2]
+			)
+		)
+			
+		Format "\t\"skinIndices\" : [" to:ostream
+		num = output.count
+		
+		if num > 0 then
+		(
+			for i = 1 to num do
+			(
+				Format "%" (newIndices[output[i] + 1]) to:ostream
+				if i < num then
+				(
+					Format "," to:ostream
+				)
+			)
+		)
+		
+		Format "],\n\n" to:ostream
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Dump skin weights
+	-- src = #( weight1, weight2, .. )
+	
+	function DumpWeights src =
+	(
+		Format "\t\"skinWeights\" : [" to:ostream
+		num = src.count
+		
+		if num > 0 then
+		(
+			for i = 1 to num do
+			(
+				Format "%" src[i] to:ostream
+				if i < num then Format "," to:ostream
+			)
+		)
+		
+		Format "],\n\n" to:ostream
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Dump the keyframes for every bone
+	-- src = #( #( parent, #( time, #( posx, posy, posz ), rot, scl ), .. ), .. )
+	--          ||---Bone-- |---------------Keyframe----------------| ----||
+	-- boneOrder lists the correct output order of the bones
+	-- newIndices is inverse of boneOrder
+	
+	function DumpKeyframes src boneOrder newIndices fps =
+	(
+		Format animHeaderFormat fps src[1][2][src[1][2].count][1] to:ostream
+		
+		numBones = boneOrder.count
+		
+		for i = 1 to (numBones) do
+		(
+			if (src[boneOrder[i]][1] == -1) then
+			(
+				parent_index = -1
+			) else
+			(
+				parent_index = newIndices[src[boneOrder[i]][1]+1]
+			)
+			
+			Format animBoneHeaderFormat parent_index to:ostream
+			
+			bnkeys = src[boneOrder[i]][2]
+			
+			for j = 1 to bnkeys.count do
+			(
+				Format keyFormat bnkeys[j][1] bnkeys[j][2][1] bnkeys[j][2][2] bnkeys[j][2][3] bnkeys[j][3].x bnkeys[j][3].y bnkeys[j][3].z bnkeys[j][3].w bnkeys[j][4] to:ostream
+				
+				if j < bnkeys.count then Format "," to:ostream
+				Format "\n" to:ostream
+			)
+			
+			Format animBoneFooterFormat to:ostream
+			
+			if i < (numBones) then
+			(
+				Format "," to:ostream
+			)
+			Format "\n" to:ostream
+		)
+		
+		Format animFooterFormat to:ostream
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Dump the morphtargets
+	-- src = #( #( #(index, name, vertices = #( #( x,y,z ), .. ) ), .. ), .. )
+	--        |List of meshes ----------------------------------------------|
+	--        |- |List of targets: only one mesh may have multiple ---| ----|
+	--        |- |- |Individual target(s) ----------------------| ----| ----|
+	
+	function DumpMorphTargets src =
+	(
+		-- This procedure assumes that only one element of src has actual morph targets.
+		-- The targets field of the other elements is used to store the vertices of static meshes.
+		-- These vertices are duplicated and joined with the vertices of the actual morph targets.
+		
+		-- Initialize with whatever happens to be first
+		
+		output = src[1]
+
+		for m=2 to src.count do
+		(
+			
+			if (src[m].count == 1) then
+			(
+				-- This is a static mesh; attach its vertices, but do nothing else.
+				
+				for t=1 to output.count do
+				(
+					join output[t][3] src[m][1][3]
+				)
+			) else (
+				-- This mesh contains morph targets.
+				-- Duplicate the static vertices, join with each morph target, and set the indices and names.
+
+				while ( output.count < src[m].count ) do
+				(
+					-- Duplicate vertices
+					
+					append output (deepCopy output[1])
+				)
+				
+				for t=1 to src[m].count do
+				(
+					
+					-- Vertices
+					join output[t][3] src[m][t][3]
+					
+					-- Index, name
+					output[t][1] = src[m][t][1]
+					output[t][2] = src[m][t][2]
+
+				)
+			)
+		)
+		
+		Format "\"morphTargets\": [" to:ostream
+		
+		for k=1 to output.count do
+		(
+			target = output[k]
+			
+			Format "{ \"name\": \"morph_%\", \"vertices\": [" target[2] to:ostream
+			
+			vertices = target[3]
+			
+			for j=1 to vertices.count do
+			(
+				Format "%,%,%" vertices[j][1] vertices[j][2] vertices[j][3] to:ostream
+				if (j != vertices.count) then
+				(
+					Format "," to:ostream
+				)
+			)
+			
+			Format "] }" to:ostream
+			if (k != output.count) then
+			(
+				Format ",\n" to:ostream
+			)
+		)
+		
+		Format "],\n" to:ostream
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Export materials
+
+	function ExportMaterials zmaterials zcolors =
+	(
+
+		Format "\"materials\": [\n" to:ostream
+
+		totalMaterials = zmaterials.count
+
+		for i = 1 to totalMaterials do
+		(
+			mat = zmaterials[i]
+
+			Format "{\n" to:ostream
+
+			-- debug
+
+			Format "\"DbgIndex\" : %,\n" (i-1) to:ostream
+
+			if classof mat != BooleanClass then
+			(
+
+				useVertexColors = zcolors[i]
+
+				Format "\"DbgName\"  : \"%\",\n" mat.name to:ostream
+
+				-- colors
+
+				DumpColor mat.diffuse  "colorDiffuse"
+				DumpColor mat.ambient  "colorAmbient"
+				DumpColor mat.specular "colorSpecular"
+
+				t = mat.opacity / 100
+				s = mat.glossiness
+
+				Format "\"transparency\"  : %,\n" t to:ostream
+				Format "\"specularCoef\"  : %,\n" s to:ostream
+
+				-- maps
+
+				DumpMap mat.diffuseMap  "mapDiffuse"
+				DumpMap mat.ambientMap  "mapAmbient"
+				DumpMap mat.specularMap "mapSpecular"
+				DumpMap mat.bumpMap 	"mapBump"
+				DumpMap mat.opacityMap 	"mapAlpha"
+
+			)
+			else
+			(
+
+				useVertexColors = false
+
+				Format "\"DbgName\"  : \"%\",\n" "dummy" to:ostream
+
+				DumpColor red "colorDiffuse"
+
+			)
+
+			Format "\"vertexColors\" : %\n" useVertexColors to:ostream
+			Format "}" to:ostream
+
+			if ( i < totalMaterials ) then Format "," to:ostream
+			Format "\n\n" to:ostream
+
+		)
+
+		Format "],\n\n" to:ostream
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Extract vertices from mesh
+
+	function ExtractVertices obj whereto =
+	(
+
+		n = obj.numVerts
+
+		for i = 1 to n do
+		(
+
+			v = GetVert obj i
+			append whereto v
+
+		)
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Extract vertex colors from mesh
+
+	function ExtractColors obj whereto =
+	(
+
+		nColors = GetNumCPVVerts obj
+
+		if nColors > 0 then
+		(
+
+			for i = 1 to nColors do
+			(
+
+				c = GetVertColor obj i
+				append whereto c
+
+			)
+
+		)
+
+	)
+
+
+	-------------------------------------------------------------------------------------
+	-- Extract normals from mesh
+
+	function ExtractNormals obj whereto needsFlip =
+	(
+
+		if smoothNormal.checked then
+		(
+
+			num = obj.numVerts
+
+			for i = 1 to num do
+			(
+
+				n = GetNormal obj i
+
+				if flipFace.checked or needsFlip then
+				(
+					n.x *= -1
+					n.y *= -1
+					n.z *= -1
+				)
+
+				append whereto n
+
+			)
+
+		)
+		else
+		(
+
+			num = obj.numFaces
+
+			for i = 1 to num do
+			(
+
+				n = GetFaceNormal obj i
+
+				if flipFace.checked or needsFlip then
+				(
+					n.x *= -1
+					n.y *= -1
+					n.z *= -1
+				)
+
+				append whereto n
+
+			)
+
+		)
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Extract uvs from mesh
+
+	function ExtractUvs obj whereto =
+	(
+		n = obj.numTVerts
+
+		for i = 1 to n do
+		(
+
+			v = GetTVert obj i
+			append whereto v
+
+		)
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Extract faces from mesh
+
+	function ExtractFaces objMesh objMaterial whereto allMaterials needsFlip hasVColors offsetVert offsetUv offsetColor =
+	(
+		n = objMesh.numFaces
+		hasUVs = objMesh.numTVerts > 0
+
+		useMultiMaterial = false
+		materialIDList = #()
+
+		materialClass = classof objMaterial
+
+		if materialClass == StandardMaterial then
+		(
+
+			fm = findItem allMaterials objMaterial
+
+		)
+		else if materialClass == MultiMaterial then
+		(
+
+			useMultiMaterial = true
+
+			for i = 1 to n do
+			(
+
+				mID = GetFaceMatID objMesh i
+				materialIndex = findItem objMaterial.materialIDList mID
+
+				if materialIndex > 0 then
+				(
+
+					subMaterial = objMaterial.materialList[materialIndex]
+
+					mMergedIndex = findItem allMaterials subMaterial
+
+					if mMergedIndex > 0 then
+					(
+
+						materialIDList[mID] = mMergedIndex
+
+					)
+					else
+					(
+
+						materialIDList[mID] = findItem allMaterials false
+
+					)
+
+				)
+				else
+				(
+
+					materialIDList[mID] = findItem allMaterials false
+
+				)
+
+			)
+
+		)
+		else
+		(
+
+			-- undefined material
+
+			fm = findItem allMaterials false
+
+		)
+
+		for i = 1 to n do
+		(
+
+			zface = #()
+
+			fv = GetFace objMesh i
+
+			fv.x += offsetVert
+			fv.y += offsetVert
+			fv.z += offsetVert
+
+			if useMultiMaterial then
+			(
+
+				mID = GetFaceMatID objMesh i
+				fm = materialIDList[mID]
+
+			)
+
+			if hasUVs then
+			(
+
+				fuv = GetTVFace objMesh i
+
+				fuv.x += offsetUv
+				fuv.y += offsetUv
+				fuv.z += offsetUv
+
+			)
+			else
+			(
+
+				fuv = false
+
+			)
+
+			if hasVColors then
+			(
+
+				fc = GetVCFace objMesh i
+
+				fc.x += offsetColor
+				fc.y += offsetColor
+				fc.z += offsetColor
+
+			)
+			else
+			(
+
+				fc = false
+
+			)
+
+			append zface fv
+			append zface fuv
+			append zface fm
+			append zface fc
+			append zface needsFlip
+
+			append whereto zface
+
+		)
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Extract materials from eventual multi-material
+
+	function ExtractMaterials objMesh objMaterial whereto wheretoColors zname hasVColors =
+	(
+
+		materialClass = classof objMaterial
+
+		if materialClass == StandardMaterial then
+		(
+
+			if ( findItem whereto objMaterial ) == 0 then
+			(
+
+				append whereto objMaterial
+				append wheretoColors hasVColors
+
+			)
+
+		)
+		else if materialClass == MultiMaterial then
+		(
+
+			n = objMesh.numFaces
+
+			for i = 1 to n do
+			(
+
+				mID = getFaceMatId objMesh i
+				materialIndex = findItem objMaterial.materialIDList mID
+
+				if materialIndex > 0 then
+				(
+
+					subMaterial = objMaterial.materialList[materialIndex]
+
+					if ( findItem whereto subMaterial ) == 0 then
+					(
+
+						append whereto subMaterial
+						append wheretoColors hasVColors
+
+					)
+
+				)
+
+			)
+
+		)
+		else
+		(
+
+			-- unknown or undefined material
+
+			append whereto false
+			append wheretoColors false
+
+		)
+
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Hack to figure out if normals are messed up
+
+	function NeedsFaceFlip node =
+	(
+		needsFlip = false
+
+		local tmp = Snapshot node
+
+		face_normal = normalize ( getfacenormal tmp 1 )
+
+		face = getface tmp 1
+
+		va = getvert tmp face[1]
+		vb = getvert tmp face[2]
+		vc = getvert tmp face[3]
+
+		computed_normal = normalize ( cross (vc - vb)  (va - vb) )
+
+		if distance computed_normal face_normal > 0.1 then needsFlip = true
+
+		delete tmp
+
+		return needsFlip
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Extract only things that either already are or can be converted to meshes
+
+	function ExtractMesh node =
+	(
+
+		if SuperClassOf node == GeometryClass then
+		(
+			needsFlip = false
+			hasVColors = false
+
+			zmesh = SnapshotAsMesh node
+
+			if autoflipFace.checked then
+			(
+
+				needsFlip = NeedsFaceFlip node
+
+			)
+
+			if exportColor.checked and ( getNumCPVVerts zmesh ) > 0 then
+			(
+
+				hasVColors = true
+
+			)
+
+			return #( zmesh, node.name, node.material, needsFlip, hasVColors )
+
+		)
+
+		-- Not geometry ... could be a camera, light, etc.
+
+		return #( false, node.name, 0, false, false )
+
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Extract the morph targets
+	-- whereto = #( #( #(index, name, vertices = #( #( x,y,z ), .. ) ), .. ), .. )
+	--            |List of meshes -----------------------------------------------|
+	--            |- |List of targets -------------------------------------| ----|
+	--            |- |- |Individual target --------------------------| ----| ----|
+
+	function ExtractMorphTargets node whereto &morphFlag = 
+	(
+		targets = #()
+		morphs = #()
+		
+		if ( node.modifiers[#morpher] != undefined ) then (
+			-- Export the morph target, if one exists
+			
+			morphFlag = true
+			
+			for i=1 to 100 do
+			(
+				nPts = WM3_MC_NumMPts node.morpher i
+				if (nPts > 0) then
+				(
+					append targets #(i, nPts)
+				)
+			)
+			
+			--Set all to zero
+			for k=1 to targets.count do
+			(
+				i = targets[k][1]
+				node.morpher[i].controller.value = 0
+			)
+			
+			--Max out one at a time, record it, then zero out again
+			for k=1 to targets.count do
+			(
+				i = targets[k][1]
+				numVerts = targets[k][2]
+				name = WM3_MC_GetName node.morpher i
+				verts = #()
+
+				node.morpher[i].controller.value = 100
+
+				for j = 1 to numVerts do
+				(
+					p = GetVert node j
+					append verts #(p.x, p.y, p.z)
+				)
+				
+				node.morpher[i].controller.value = 0
+				
+				append morphs #(i, name, verts)
+			)
+			append whereto morphs
+		) else (
+			-- Export the mesh vertices as a dummy morph target
+			
+			verts = #()
+			for k=1 to node.numVerts do
+			(
+				p = GetVert node k
+				append verts #(p.x, p.y, p.z)
+			)
+			
+			dummy = #()
+			append dummy #(0, "DUMMY", verts)
+			append whereto dummy
+		)
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Transforms the matrix of a bone into its parent space, if a parent exists.
+	
+	function thinkLocally bone_node localForm =
+	(
+		localForm = bone_node.transform
+		
+		if ( bone_node.parent != undefined ) then
+		(
+			parentForm = bone_node.parent.transform
+			localForm = localForm * inverse parentForm
+		)
+		
+		newLocal = matrix3 1
+		
+		px = localForm.translationpart.x
+		py = localForm.translationpart.y
+		pz = localForm.translationpart.z
+		lTran = transMatrix (localForm.translationpart)
+		
+		lRot = (inverse localForm.rotationpart) as matrix3
+		
+		lScale = scaleMatrix localForm.scalepart
+		
+		localForm = lScale * lRot * lTran * newLocal
+		
+		localForm
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Extract bones and keyframes
+
+	function ExtractAnimation node bones keyframes FPS bone_names &skinFlag =
+	(
+		if node.modifiers[#skin] != undefined then
+		(
+			skinFlag = true
+			
+			---------------------------------------------------------------------------------
+			-- A dummy root bone is first created and roatated to orient the model
+			
+			p = (matrix3 1).translationpart
+			s = (matrix3 1).scalepart
+			r = (matrix3 1).rotationpart
+			
+			append bones #(-1,"flipRoot",p,s,r)
+
+			/*
+			if (flipYZ.checked) then
+			(
+				r = threeMatrix.rotationpart
+			) else (
+				r = (matrix3 1).rotationpart
+			)
+			*/
+			
+			r = threeMatrix.rotationpart
+			
+			root_keys = #(#(0, p, r, s))
+			
+			slidertime = 0
+			
+			while (slidertime < animationrange.end) do
+			(
+				slidertime += 1
+				sTime = (slidertime / FPS) as String
+				
+				append root_keys #(substring sTime 1 (sTime.count - 1), p, r, s)
+			)
+			
+			append keyframes #(-1, root_keys)
+			
+			---------------------------------------------------------------------------------
+			-- The model's bones and keyframes are then extracted
+			
+			max modify mode
+			
+			total_bones = skinops.getnumberbones node.modifiers[#skin]
+			
+			vertex_count = getNumverts node
+			
+			-- Find parents by looking up their names; bone names MUST be unique			
+			-- Can't guarantee that parent will be read before child; store all names beforehand
+			for i = 1 to total_bones do
+			(
+				bone_name = skinops.getbonename node.modifiers[#skin] i 0
+				append bone_names bone_name
+			)
+			
+			for i = 1 to total_bones do
+			(
+				slidertime = 0
+				
+				bone_name = skinops.getbonename node.modifiers[#skin] i 0
+				bone_node = getNodeByName bone_name
+				
+				parent_index = 0
+				if ( bone_node.parent != undefined ) then
+				(
+					parent_name = bone_node.parent.name
+					parent_index = (findItem bone_names parent_name)
+				)
+				
+				localForm = bone_node.transform
+				localForm = thinkLocally bone_node localForm
+
+				p = localForm.translationpart
+				r = localForm.rotationpart
+				s = localForm.scalepart
+				
+				append bones #(parent_index, bone_name, p, bone_node.transform.scalepart, r)
+				
+				in coordsys parent bone_keys = #(#(0, p, r, bone_node.transform.scalepart))
+				
+				while (slidertime < animationrange.end) do
+				(
+					slidertime += 1
+					sTime = (slidertime / FPS) as String
+					
+					localForm = bone_node.transform
+					localForm = thinkLocally bone_node localForm
+					
+					p = localForm.translationpart
+					r = localForm.rotationpart
+					s = localForm.translationpart
+					
+					append bone_keys #(substring sTime 1 (sTime.count - 1), p, r, bone_node.transform.scalepart)
+				)
+				append keyframes #(parent_index, bone_keys)
+			)
+		)			
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Extract the skin indices and weights in one pass
+	-- If it's a skin, skinned? = true and indices contains the bones
+	-- If it's not, indices is dummied to #(0,..) and DumpIndices uses the parent to fix it in post
+	-- indices = #( #( skinned?, indices, parent), ..)
+
+	function ExtractInfluences node indices weights =
+	(
+		vertex_count = getNumverts node
+		
+		meshIndices = #()
+		
+		if node.modifiers[#skin] != undefined then
+		(
+			
+			for i = 1 to vertex_count do
+			(
+				-- Insane defaults for the sort; these shouldn't escape into the output
+				index1 = -1
+				index2 = -1
+				weight1 = -1
+				weight2 = -1
+				
+				numBones = skinOps.GetVertexWeightCount node.modifiers[#skin] i
+				
+				--Two passes of a bubble sort to get the 2 heaviest weights
+				for j = 1 to numBones do
+				(
+					thisIndex = skinops.getVertexWeightBoneID node.modifiers[#skin] i j
+					thisWeight = skinops.getvertexweight node.modifiers[#skin] i j
+					
+					if (thisWeight) > weight1 then
+					(
+						weight1 = thisWeight
+						index1 = thisIndex
+					)
+				)
+				
+				for j = 1 to numBones do
+				(
+					thisIndex = skinops.getVertexWeightBoneID node.modifiers[#skin] i j
+					thisWeight = skinops.getvertexweight node.modifiers[#skin] i j
+					
+					if ((thisWeight > weight2) and (thisIndex != index1)) then
+					(
+						weight2 = thisWeight
+						index2 = thisIndex
+					)
+				)
+				
+				-- Establish legal defaults: no weight from the root
+				if (index1 == -1) then
+				(
+					index1 = 0
+					weight1 = 0
+				)
+				if (index2 == -1) then
+				(
+					index2 = 0
+					weight2 = 0
+				)
+				
+				
+				append meshIndices (index1)
+				append meshIndices (index2)
+				
+				append weights weight1
+				append weights weight2
+
+			)
+			
+			append indices #(true, meshIndices, "ROOT")
+			
+		) else (
+			for i = 1 to vertex_count do
+			(
+				append meshIndices 0
+				append meshIndices 0
+				
+				append weights 1
+				append weights 1
+			)
+			
+			name = "Scene Root"
+			if node.parent != undefined then
+			(
+				name = node.parent.name
+			)
+			
+			append indices #(false, meshIndices, name)
+		)
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Enforce that parent is above all of its children in the output
+	-- This fixes several amusing bugs (mostly fingers of infinite length)
+	--
+	-- boneOrder: order to dump the bones in #(bones)
+	-- newIndices: new positional indices of bones
+	
+	function ReorderBones bones boneOrder newIndices =
+	(
+	
+		/*************************************************************************************
+		 * Reorder bones
+		 * Python function prototype:
+		 * for i in range(n):
+		 *	for b in range(n):
+		 *		#new bone                   parent of bone legal
+		 *		if not inOut[b] and (parents[b] == -1 or inOut[parents[b]]):
+		 *			inOut[b] = True
+		 *			boneOrder.append(b)
+		 *			break;
+		 *************************************************************************************/
+		
+		total_bones = bones.count
+		
+		-- Keeps track of which parents have been accounted for
+		inOut = #()
+		
+		for i = 1 to total_bones do
+		(
+			append inOut false
+		)
+		
+		rootNotAdded = true
+		for i = 1 to total_bones do
+		(
+			
+			notFound = true
+			for b = 1 to total_bones while notFound do
+			(
+				if (inOut[b] != true) then
+				(
+					if (rootNotAdded and bones[b][1] == -1) then
+					(
+						inOut[b] = true
+						append boneOrder b
+						notFound = false
+						rootNotAdded = false
+					) else (
+						if (inOut[bones[b][1] + 1]) then
+						(
+							inOut[b] = true
+							append boneOrder b
+							notFound = false
+						)
+					)
+				)
+			)
+
+		)
+		
+		-- Takes original bone index/parent + 1, returns new correct index for parent, skinIndices, etc
+		for i=1 to total_bones do
+		(
+			newIndices[boneOrder[i]] = i - 1
+		)
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Export scene
+	-- 
+	-- This will BREAK in HORRIBLE WAYS if you feed it more than one object for now.
+
+	function ExportScene =
+	(
+
+		-- Extract meshes
+
+		meshObjects = #()
+
+		mergedVertices = #()
+		mergedNormals = #()
+		mergedColors = #()
+
+		mergedUvs = #()
+		mergedFaces = #()
+
+		mergedMaterials = #()
+		mergedMaterialsColors = #()
+
+		sceneHasVColors = false
+		
+		hasSkin = false
+		bones = #()
+		keyframes = #()
+		
+		influences = #()
+		weights = #()
+		
+		boneOrder = #()
+		newIndices = #()
+		bone_names = #()
+		
+		hasMorph = false
+		mergedMorphTargets = #()
+
+		-- The horrible hackery that is skinops requires only one object be selected.
+		original_selection = #()
+		for obj in selection do
+		(
+			append original_selection obj.name
+		)
+		
+		max select none
+		
+		for name in original_selection do
+		(
+			obj = getnodebyname name
+			select obj
+
+			result = ExtractMesh obj
+			meshObj = result[1]
+
+			if ClassOf meshObj == TriMesh then
+			(
+
+				meshName     = result[2]
+				meshMaterial = result[3]
+				needsFlip    = result[4]
+				hasVColors   = result[5]
+
+				sceneHasVColors = sceneHasVColors or hasVColors
+
+				append meshObjects result
+
+				vertexOffset = mergedVertices.count
+				uvOffset = mergedUvs.count
+				colorOffset = mergedColors.count
+
+				ExtractMaterials meshObj meshMaterial mergedMaterials mergedMaterialsColors meshName hasVColors
+
+				ExtractVertices meshObj mergedVertices
+				ExtractNormals meshObj mergedNormals needsFlip
+				ExtractColors meshObj mergedColors
+
+				ExtractUvs meshObj mergedUvs
+
+				ExtractFaces meshObj meshMaterial mergedFaces mergedMaterials needsFlip hasVColors vertexOffset uvOffset colorOffset
+
+				ExtractAnimation obj bones keyframes fps.value bone_names &hasSkin
+				
+				ExtractInfluences obj influences weights
+				
+				ReorderBones bones boneOrder newIndices
+				
+				ExtractMorphTargets obj mergedMorphTargets &hasMorph
+
+			)
+			
+			max select none
+		)
+
+		totalVertices = mergedVertices.count
+		totalFaces = mergedFaces.count
+		totalMaterials = mergedMaterials.count
+
+		totalColors = 0
+		totalNormals = 0
+		totalUvs = 0
+
+		useColors = false
+
+		if sceneHasVColors and exportColor.checked then
+		(
+
+			totalColors = mergedColors.count
+			useColors = true
+
+		)
+
+		if exportNormal.checked then
+		(
+
+			totalNormals = mergedNormals.count
+
+		)
+
+		if exportUv.checked then
+		(
+
+			totalUvs = mergedUvs.count
+
+		)
+
+		-- Dump model
+
+		Format "{\n\n" to:ostream
+
+		-- Dump header
+
+		Format headerFormat maxFileName totalVertices totalNormals totalColors totalUvs totalFaces totalMaterials to:ostream
+
+		-- Dump all materials in the scene
+
+		ExportMaterials mergedMaterials mergedMaterialsColors
+
+		-- Dump merged data from all selected geometries
+
+		DumpVertices mergedVertices
+		
+		if hasMorph then
+		(
+			DumpMorphTargets mergedMorphTargets
+		)
+		
+		DumpNormals mergedNormals
+		DumpColors mergedColors useColors
+		DumpUvs mergedUvs
+		DumpFaces mergedFaces useColors
+		
+		if hasSkin then
+		(
+			DumpBones bones boneOrder newIndices
+			DumpIndices influences bone_names newIndices
+			DumpWeights weights		
+			DumpKeyframes keyframes boneOrder newIndices fps.value
+		)
+
+		-- Dump footer
+
+		Format footerFormat to:ostream
+
+	)
+	
+	-------------------------------------------------------------------------------------
+	-- Open and prepare a file handle for writing
+
+	function GetSaveFileStream =
+	(
+		zname = getFilenameFile maxFileName
+		zname += ".js"
+
+		fname = GetSaveFileName filename:zname types:"JavaScript file (*.js)|*.js|All Files(*.*)|*.*|"
+		if fname == undefined then
+		(
+
+			return undefined
+
+		)
+
+		ostream = CreateFile fname
+		if ostream == undefined then
+		(
+
+			MessageBox "Couldn't open file for writing !"
+			return undefined
+
+		)
+
+		return ostream
+	)
+
+	-------------------------------------------------------------------------------------
+	-- Export button click handler
+
+	on btn_export pressed do
+	(
+		ostream = GetSaveFileStream()
+		if ostream != undefined then
+		(
+
+			ExportScene()
+			close ostream
+
+		)
+
+	)
+
+)
+createDialog ThreeJSExporter width:300

Some files were not shown because too many files changed in this diff