------------------------------------------------------------------
-- Wavefront OBJ/MTL Import Utility	for gmax, v1.1				--
-- by Chris Cookson (cjcookson@hotmail.com)						--
--																--
-- All code (c) Copyright Chris Cookson, 2001					--
-- Please use this script any way you wish - just				--
-- drop me a line if you do something cool with it!				--
-- 																--
-- With thanks to Jack Palevich									--
--																--
-- Changelog:													--
--																--
-- v1.1(a)	Fixed bug with importing objects with no materials	--
-- v1.1		Should now work with 3DSMAX R3/4					--
--																--
------------------------------------------------------------------

utility objImport "Wavefront OBJ Import"
(
	----------
	-- Vars --
	----------
	local currentPath=""

	-------------
	-- Structs --
	-------------
	struct Tokenizer
	(
		tokens = #(),
		
		fn SetString str=
		(
			tokens = filterString str "\t ,"
		),
		
		fn ReadToken=
		(
			if tokens.count > 0 then
			(
				local tok = tokens[1]
				deleteItem tokens 1
				tok
			) else
			(
				undefined
			)
		),
		
		fn PeekToken=
		(
			if tokens.count > 0 then tokens[1] else undefined
		)
	)
	
	--------------------
	-- User Interface --
	--------------------
	group "About"
	(
		label lab1 "Wavefront OBJ Importer v1.1a"
		HyperLink addy "by Chris Cookson" align:#center address:"mailto:cjcookson@hotmail.com" color:(color 0 0 170) hoverColor:(color 170 0 0)
	)
		
	group "Import"
	(
		checkBox autoSizeCheckBox "Rescale" checked:false align:#center
		spinner autoSizeSpinner "Scale by:" align:#center type:#float range:[0,10000,64] enabled:false
		button importButton "Import..."
	)
	
	----------------------
	-- Helper Functions --
	----------------------

	-- Read a float from a tokenizer with default fallback
	function ReadFloat tkzr default:0.0 =
	(
		local floatStr = tkzr.ReadToken()
		if floatStr != undefined then
			return floatStr as float
		else
			return default
	)
	
	-- Read an integer from a tokenizer with default fallback
	function ReadInt tkzr default:0 =
	(
		local intStr = tkzr.ReadToken()
		if intStr != undefined then
			return intStr as float
		else
			return default
	)
	
	-- Read a face vertex definition of the form v/t/n where t or n are optional
	function ReadFaceVertex tkzr =
	(
		local token = tkzr.ReadToken()
		local vdef = filterString token "/"

		local v = vdef[1] as integer
		local t = 0
		local n = 0
		
		-- Is there a texcoord or just a blank?
		if (vdef.count > 1) and (findString token "//") == undefined then
		(
			t = vdef[2] as integer
			if vdef.count == 3 then n = vdef[3] as integer
		) else
		(
			if vdef.count == 2 then n = vdef[2] as integer
		)
		
		[ v, t, n ]
	)
	
	-- Load a MTL material library definition file and return a gmax multi material
	function LoadMaterialLibrary filename: =
	(
		local newMaterial = multimaterial numsubs:0

		if filename == undefined then
		(
			-- Return a default MAX material
			newMaterial.numsubs = 1
			newMaterial[1] = standard name:"Unnamed"
			return newMaterial
		)
		
		-- Check file exists
		if (getFiles filename).count == 0 then
		(
			format "Error: Cannot find material file %\n" filename
			newMaterial.numsubs = 1
			newMaterial[1] = standard name:(getFilenameFile filename)
			return newMaterial
		)
		
		newMaterial.name = (getFilenameFile filename)
		
		local curIndex = 0
		
		mtlStream = openFile filename mode:"r"

		try
		(
			local materialName
			local curMaterialID = -1
			
			local diffuse, specular, ambient, specularPower, diffuseMap
			
			while not (eof mtlStream) do
			(
				local lineTokenizer = Tokenizer()
				local curLine = readLine mtlStream
				lineTokenizer.SetString curLine
				local idToken = lineTokenizer.ReadToken()
				
				case idToken of
				(
					"newmtl": -- Define a new material
					(
						local materialName = lineTokenizer.ReadToken()
						format "Loading material %\n" materialName
						
						curIndex += 1
						newMaterial.numsubs += 1
						newMaterial[curIndex] = standard name:materialName
					)
					
					"Kd": -- Diffuse reflectivity
					(
						local red = ReadFloat lineTokenizer
						local green = ReadFloat lineTokenizer
						local blue = ReadFloat lineTokenizer
						diffuse = color (red*255) (green*255) (blue*255)
						newMaterial[curIndex].diffuse = diffuse
					)
					
					"Ks": -- Specular reflectivity
					(
						red = ReadFloat lineTokenizer
						green = ReadFloat lineTokenizer
						blue = ReadFloat lineTokenizer
						specular = color (red*255) (green*255) (blue*255)
						newMaterial[curIndex].specular = specular
					)
					
					"Ka": -- Ambient reflectivity
					(
						local red = ReadFloat lineTokenizer
						local green = ReadFloat lineTokenizer
						local blue = ReadFloat lineTokenizer
						ambient = color (red*255) (green*255) (blue*255)
						newMaterial[curIndex].ambient = ambient						
					)
					
					"Ns": -- Specular power
					(
						specularPower = ReadFloat lineTokenizer
						newMaterial[curIndex].specularLevel = specularPower
					)
					
					"map_Kd": -- Diffuse texture map
					(
						diffuseMapFile = curLine
						local pos = findString diffuseMapFile "map_Kd"
						diffuseMapFile = substring diffuseMapFile (pos + 6) (diffuseMapFile.count - pos - 5)
						diffuseMapFile = trimRight diffuseMapFile
						diffuseMapFile = trimLeft diffuseMapFile 
						
						if (getFiles diffuseMapFile).count != 0 then
						(
							-- Texture map file exists, so set it up and make it visible in viewports
							format "Loading diffuse texture map '%'\n" diffuseMapFile
							local diffuseMap = bitmapTexture filename:diffuseMapFile name:(getFilenameFile diffuseMapFile)
							newMaterial[curIndex].maps[2] = diffuseMap	
							newMaterial[curIndex].mapEnables[2] = true
							showTextureMap newMaterial[curIndex] diffuseMap true
						) else
						(
							-- Can't find texture map file :(
							format "Warning: Cannot find texture map file %\n" diffuseMapFile
							newMaterial[curIndex].mapEnables[2] = false
						)
					)
				)	
			)
		)
		catch
		(
			messageBox "Error loading material file!"
			throw
		)
			
		close mtlStream
		
		newMaterial		-- No need for return!
	)

	--------------------------
	-- Main Import Function --
	--------------------------

	on importButton pressed do
	(
	
		--- Imported Data ---
	
		local verts = #()
		local faces = #()
		local normals = #()
	
		local texCoords = #()
		local texFaces = #()
		
		local faceMaterialIDs = #()
		local faceGroupIDs = #()
	
		local materialNames = #()
	
		local objectName = undefined
		local materialLib = undefined

		-- Open up a dialog box
		objFileName = getOpenFileName caption:"Import OBJ" \
					types:"Wavefront OBJ (*.obj)|*.obj|All Files (*.*)|*.*|"

		-- If user made a selection, begin importing
		if objFileName != undefined then
		(
			currentPath = getFilenamePath objFileName
		
			-- Open up the file
			objStream = openFile objFileName mode:"r"
					
			local curMaterialID = 0
			local lineNum = 0
			local isValid = true
		
			try
			(
				-- Go through the whole OBJ file
				while not (eof objStream) do
				(
					local lineTokenizer = Tokenizer()
					
					lineTokenizer.SetString (readLine objStream)
					
					lineNum += 1
					
					-- Read the data ID tag
					local idToken = lineTokenizer.ReadToken()
					
					if idToken != undefined do
					(
	
						case idToken of
						(
							"o": -- Object name
							(
								objectName = lineTokenizer.ReadToken()
							)
							
							"g": -- Define group
							(
								-- Ignore groups for now
							)
							
							"mtllib": -- Set material library file
							(
								materialLib = lineTokenizer.ReadToken()
							)
							
							"usemtl": -- Use specified material
							(
								local matName = lineTokenizer.ReadToken()
								
								-- Have we seen this material name before?
								local index = findItem materialNames matName
								if index == 0 then
								(
									-- No, add it to our list
									append materialNames matName
									curMaterialID = materialNames.count
								) else
								(
									-- Yes, we already have an ID
									curMaterialID = index
								)
							)
							
							"v": -- Set vertex co-ordinate
							(
								local x = ReadFloat lineTokenizer
								local y = ReadFloat lineTokenizer
								local z = ReadFloat lineTokenizer
								append verts [ x, y, z ]
							)
							
							"vt": -- Set vertex texcoord
							(
								local u = ReadFloat lineTokenizer
								local v = ReadFloat lineTokenizer
								local w = ReadFloat lineTokenizer
								append texcoords [ u, v, w ]
							)
		
							"vn": -- Set vertex normal
							(
								local nx = ReadFloat lineTokenizer
								local ny = ReadFloat lineTokenizer
								local nz = ReadFloat lineTokenizer
								append normals [ nx, ny, nx ]
							)
							
							"f": -- Define face (coord index/texcoord index/normal index)
							(
								local v1 = ReadFaceVertex lineTokenizer
								local v2 = ReadFaceVertex lineTokenizer
								local v3 = ReadFaceVertex lineTokenizer
								
								-- Lightwave sometimes exports 4 sided polys
								if (lineTokenizer.PeekToken()) != undefined then
								(
									local v4 = ReadFaceVertex lineTokenizer
	
									-- Split 4 sided poly into two tris
									append faces [ v1[1], v3[1], v4[1] ]
									append faceMaterialIDs curMaterialID
									append texFaces [ v1[2], v3[2], v4[2] ]
								)
																						
								-- TODO: Add support for n-sided planar polys
							
								append faces [ v1[1], v2[1], v3[1] ]
								append faceMaterialIDs curMaterialID
								append texFaces [ v1[2], v2[2], v3[2] ]
							)
							
							"#": -- Comment (ignore)
							(
							)
						)
					)
				)				
			) catch
			(
				messageBox "Error reading OBJ file!"
				isValid = false
				close objStream
				throw
			)
			
			-- Close the file stream
			close objStream
			
			-- Print out a bit of info
			format "Read % verts, % faces.\n" verts.count faces.count
	
			if isValid then
			(
				-- Load and parse the material library (if present)
				if materialLib != undefined then
				(
					newMaterial = LoadMaterialLibrary filename:(currentPath + materialLib)
				) else
				(
					newMaterial = multimaterial prefix:(getFilenameFile objFileName)
					newMaterial.numsubs = 1
					newMaterial[1] = standard prefix:"Material"
				)
				
				-- Remap ad-hoc face material IDs onto gmax material IDs
				for i = 1 to faceMaterialIDs.count do
				(

					-- Make sure material IDs are valid - we can be sure there's always 1 submaterial
					if (faceMaterialIDs[i] > newMaterial.numsubs) or (faceMaterialIDs[i] < 1) then
					(
						faceMaterialIDs[i] = 1
					)
					else
					(
						local matName = materialNames[faceMaterialIDs[i]]

						-- Find corresponding gmax material
						for matID in newMaterial.materialIDList where (newMaterial[matID].name == matName) do
						(
							faceMaterialIDs[i] = matID
						)
					)
				)
				
				if objectName == undefined do
				(
					objectName = (getFilenameFile objFileName)
				)
										
				-- Construct the mesh object
				local newMesh = mesh name:objectName pos:[0,0,0]\
									 vertices:verts faces:faces \
									 material:newMaterial
									 
				-- Set face material IDs
				for i = 1 to faceMaterialIDs.count do
				(
					setFaceMatID newMesh i faceMaterialIDs[i]					
				)
				
				-- Add texcoords
				if texcoords.count > 0 do
				(
					-- Add texcoords
					setNumTVerts newMesh texcoords.count
					
					for i = 1 to texcoords.count do
					(
						setTVert newMesh i texcoords[i]
					)
				
					-- Build texcoord faces
					buildTVFaces newMesh
				
					-- Set texcoord faces
					for i = 1 to texFaces.count do
					(
						setTVFace newMesh i texFaces[i]
					)
				
				) 
				
				-- Crude scaling, need to make this a bit more intelligent
				if autoSizeCheckBox.checked then
				(
					local scale = [autoSizeSpinner.value, autoSizeSpinner.value, autoSizeSpinner.value]
					
					for i = 1 to newMesh.numverts do
					(
						local v = getvert newMesh i
						v *= scale
						setvert newMesh i v
					
					)
				)
				
				-- Update mesh
				update newMesh
							
				-- Redraw gmax viewports
				max views redraw
							
				-- Ta-daa! You should now see a lovely new imported model in all its glory.
				print "Import completed."
			)
		)
	)
	
	on autoSizeCheckBox changed nowChecked do
	(
		autoSizeSpinner.enabled = nowChecked
	)
)

