------------------------------------------------------------------------------------- -- ThreeJSExporter.ms -- Exports geometry from 3ds max to Three.js models in ASCII JSON format v3 -- By alteredq / http://alteredqualia.com -- -- 2014.06.25 -- Add vertex export from each frame ------------------------------------------------------------------------------------- function eav_attime obj t = ( local i local s_out = "" s_out = s_out as stringstream local zmesh = at time t (SnapshotAsMesh obj) local n = zmesh.numverts local vrs_ar = #() local v = [0,0,0] for i = 1 to n do ( v = (GetVert zmesh i) append vrs_ar v ) for i = 1 to vrs_ar.count do ( v = vrs_ar[i] format "%, %, %" v.x v.z -v.y to:s_out if i < vrs_ar.count then ( format ", " to:s_out ) ) return (s_out as string) ) /* TODO 2014.06.25 Export animation from modifiers */ function eav_get_range_from_trans_con obj &i_t1 &i_t2 = ( -- Get keys range from Pos, rotation, scale controllers local i local con local t1min = 0, t2max = 0 for i = 1 to 3 do ( con = obj.controller[i] format "\nController: %" obj.controller[i].name format " (keys count: %)" con.keys.count if con.keys.count == 0 then ( continue ) t1 = con.keys[1].time.frame as integer t2 = (con.keys[con.keys.count].time.frame) as integer if i == 1 then ( t1min = t1 t2max = t2 ) if t1 < t1min then ( t1min = t1 ) if t2 > t2max then ( t2max = t2 ) ) i_t1 = t1min i_t2 = t2max if( i_t1 == 0 )and( i_t2 == 0 )then ( return(false) ) else ( return(true) ) ) function eav_get_range_from_mods_con obj &i_t1 &i_t2 = ( local i local cmod, mod_con local props, pr local t1min = 0, t2max = 0 -- format "\n\nModifiers:\n" for i = 1 to obj.modifiers.count do ( cmod = obj.modifiers[i] -- format "\n%: \"%\" (%)\n" i (cmod.name) (classof cmod) props = getpropnames cmod for pr in props do ( mod_con = (getPropertyController cmod pr) if mod_con == undefined then ( continue ) if mod_con.keys.count <= 0 then ( continue ) -- format "\t%\t(keys: %)\n" pr (mod_con.keys.count) t1 = mod_con.keys[1].time.frame as integer t2 = (mod_con.keys[mod_con.keys.count].time.frame) as integer if i == 1 then ( t1min = t1 t2max = t2 ) if t1 < t1min then ( t1min = t1 ) if t2 > t2max then ( t2max = t2 ) ) ) i_t1 = t1min i_t2 = t2max if( i_t1 == 0 )and( i_t2 == 0 )then ( return(false) ) else ( return(true) ) ) function eav_exp_obj obj ostream = ( local i, t1, t2, t1_m, t2_m local b_ran_set = false local b_ran_mod_set = false format "\n\n-----------------------------\nObject: \"%\"\n" obj.name -- Total range: /* local frames_num = animationRange.end.frame - animationRange.start.frame frames_num = frames_num as integer */ -- Range detection between keys: b_ran_set = eav_get_range_from_trans_con obj &t1 &t2 b_ran_mod_set = eav_get_range_from_mods_con obj &t1_m &t2_m format "\n\nKey ranges detected:\n" format " transform: (% to %) - %\n" t1 t2 b_ran_set format " modifiers: (% to %) - %\n" t1_m t2_m b_ran_mod_set if b_ran_set and b_ran_mod_set then ( -- format "\nAll ranges set - compare\n" -- Set smallest first key, and latest final key if t1_m < t1 then ( t1 = t1_m ) if t2_m > t2 then ( t2 = t2_m ) ) else if( not b_ran_set )and( b_ran_mod_set )then ( -- format "\nTrans range not set\n" t1 = t1_m t2 = t2_m ) else if( not b_ran_mod_set )and( b_ran_set )then ( -- format "\nmods range not set\n" -- all values t1, t2 - save in initial state ) else if( not b_ran_set )and( not b_ran_mod_set )then ( format "\n No key range set. Exit function\n" return(false) ) format "\n final range for export: (% to %)\n" t1 t2 ---- Output format "\n\"morphTargets\": [" to:ostream for i = t1 to t2 do ( format "\n{\"name\": \"FRAME000\", \"vertices\": [" to:ostream format "%]}" (eav_attime obj i) to:ostream if i < t2 then ( format "," to:ostream ) ) format " ],\n\n" to:ostream format "\n\n\"morphColors\": [],\n\n\n" to:ostream ) function exp_anim_verts_sel ostream = ( Clearlistener() format "\n---- Export verts:\n" for obj in selection do ( if superclassof obj != geometryclass then continue eav_exp_obj obj ostream ) format "\n----\n" ) ---- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- rollout ThreeJSExporter "ThreeJSExporter" ( -- Variables local ostream, headerFormat = "\"metadata\":{\"sourceFile\": \"%\",\"generatedBy\": \"3ds max ThreeJSExporter\",\"formatVersion\": 3.1,\"vertices\": %,\"normals\": %,\"colors\": %,\"uvs\": %,\"triangles\": %,\"materials\": %},", vertexFormat = "%,%,%", vertexNormalFormat = "%,%,%", UVFormat = "%,%", triFormat = "%,%,%,%", triUVFormat = "%,%,%,%,%,%,%", triNFormat = "%,%,%,%,%,%,%", triUVNFormat = "%,%,%,%,%,%,%,%,%,%", footerFormat = "}" ------------------------------------------------------------------------------------- -- 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:true enabled:true checkbox flipUV "Flip UV" checked:false enabled:true checkbox flipFace "Flip all faces" checked:false enabled:true checkbox autoflipFace "Try fixing flipped faces" checked:false enabled:true label dummy3 "--------------------------------------------------------" align:#left checkbox cb_exp_mt "Export Morph Targets" checked:true enabled:true 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 ) ---- 2014.06.25 16:15 function dump_morph_targets = ( Clearlistener() format "\n---- dump_morph_targets():\n" if not cb_exp_mt.state then ( format "\nNot checked\n" return() ) exp_anim_verts_sel ostream format "\n----\n" ) ------------------------------------------------------------------------------------- -- 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 "]\n\n" 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 "\"%\" : \"%\",\n" label fname 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 ) ) ------------------------------------------------------------------------------------- -- Export scene function ExportScene = ( -- Extract meshes meshObjects = #() mergedVertices = #() mergedNormals = #() mergedColors = #() mergedUvs = #() mergedFaces = #() mergedMaterials = #() mergedMaterialsColors = #() sceneHasVColors = false for obj in selection do ( 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 ) ) 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 objects (debug) -- Format "// Source objects:\n\n" to:ostream -- i = 0 -- for obj in meshObjects do -- ( -- meshName = obj[2] -- Format "// %: %\n" i meshName to:ostream -- i += 1 -- ) -- 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 ---- 2014.06.25 16:14 dump_morph_targets() ---- DumpNormals mergedNormals DumpColors mergedColors useColors DumpUvs mergedUvs DumpFaces mergedFaces useColors -- 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