Notifications
Clear all

[Closed] Recursive Relinking Bitmaps Script

Latest version available on www.scriptspot.com
Direct Link: http://www.scriptspot.com/3ds-max/relink-bitmaps

Scripters,
I was fussing about the photometric/bitmap paths plug-in because it doesn’t search recursively. I’m sure plenty of people have written a script very similar. Here is my version. Some improvements:

Update: version 1.06!
– Fixed a bug thanks Gravey!

 [b] - Fast recursive directory searching
  • Interactive Mode
    – Allows the user to individually select and isolate the objects in the scene with missing bitmaps

  • Undo availability toggle
    [/b]

    I hope this script will be useful to you in your scripting endevours!


/* 
	Relinking Bitmaps Script written by Colin Senner
	for Arnold Imaging LLC, Kansas City, MO

	- disableSceneRedraw() added
	- Interactive Mode added
	
	version 1.06
	- Fixed a crash related to the rlt_Missing being closed
			-- Thanks Gravey @ CGTalk
*/

-- Closes the previous one if the script is run more than once
if rf != undefined then closeRolloutFloater rf

-- Rollout Definition
global rf				-- Rollout Floater - Main
global rlt_Main			-- Rollout Main	- Missing Bitmaps Window toggle
global rlt_Missing		-- Rollout Missing - holds the information about missing bitmaps
global rlt_Search		-- Rollout Search  - holds the search information for the user to pick where to search

-- Variables Definition
global missingMaps=#()			-- Holds the filenames of the missingMaps - index matches missingMapsObjs
global missingMapsObjs=#()		-- Holds the objects which are missing maps - index matches missingMaps
global pathsToSearch=#()		-- Holds the paths the user wants to search

global interactiveModeOn=false	-- Holds the state of InteractiveMode

-- Function Definition
global closeRollout				-- closes rollouts gracefully
global openRollout				-- opens rollouts gracefully
global openRolloutNextTo		-- opens rollouts next to others

global getMissingMaps			-- retreives missing maps via getClassInstances() function, used only for InteractiveMode OFF
global getMissingMapsObjs		-- retreives missing maps via enumerateFiles()	function, used only for InteractiveMode ON
global getDirsRecursive 		-- gets an array of recursive directories
global getDirectoryFiles		-- gets an array of files in directories
global getPathsToSearch			-- returns the array of all the paths the user wants to search
	
global addmapObj				-- called during enumerateFiles()
global trim_dups				-- two arrays a b, searches array a for duplicates and deletes them in both arrays a b
global trim_dupsOne				-- array a, searches array a for duplicates and deletes them
global lowercase				-- converts strings to all lowercase

global relinkMaps				-- Main function for relinking the bitmaps


---------------- Functions -------------------
fn closeRollout rlt = (	if rlt.open then destroyDialog rlt )
fn openRollout rlt thewidth theheight = ( if rlt.open == false then createDialog rlt width:thewidth height:theheight )
fn openRolloutNextTo sRlt dRlt thewidth theheight = (
	if dRlt.open == false then (
		local theposx = rf.pos.x
		local theposy = rf.pos.y
		local thenewposx = (sRlt.width+27)+theposx
		createDialog dRlt width:thewidth height:theheight pos:[thenewposx,theposy]
	)	
)
fn getDirsRecursive root = (
	dir_array = GetDirectories (root+"/*")
	
	for d in dir_array do
		join dir_array (GetDirectories (d+"/*"))
	dir_array
)

-- Used for InteractiveMode OFF --
fn getMissingMaps = (
	local mapfiles = #()
	local mapfileN = #()
	local mBitmaps = getClassInstances BitmapTexture		-- gets all bitmapTextures in the scene

	mapfiles = mBitmaps										-- copies the array instance to "mapfiles"
	
	for m in mapfiles do (
		-- for every bitmap texture in the scene
		if (isProperty m #filename) then (
			-- that has a #filename property	
			if m.filename != "" then (
				-- that isn't blank
				if not (doesFileExist m.filename) then		-- if it doesn't exist, add to the array mapfileN
					append mapfileN m.filename
			)
		)
	)

	trim_dupsOne mapfileN
	--print "getMissingMaps() completed"
	mapfileN											-- returns the new array of only missing bitmaptextures
) 

-- Used for InteractiveMode ON --
fn getMissingMapsObjs = (
	missingMapsObjs=#()	
	missingMaps=#()
	
	-- called for enumerateFiles()
	fn addmapObjs map obj = (
		append missingMapsObjs obj							-- adds the object that has a missing map to the array missingMapsObjs
		append missingMaps map								-- adds the map	that is missing		to the array missingMaps
	)
	
	for o in objects where o.material != undefined do		-- cycle through the scene objects
		enumerateFiles o addmapObjs o #missing				-- find missing maps

	trim_dups missingMapsObjs missingMaps					-- trim the duplicates from missingMapsObjs and also missingMaps

	--print "getMissingMapsObjs() completed"
	missingMaps										-- return the array of only missing bitmaptextures
)

-- Gets the paths the user wants to search for textures
fn getPathsToSearch = (
	 pathsToSearch = #()
	
	append pathsToSearch rlt_Search.edt_manualSearch.text

	pathsToSearch
)

fn getDirectoryFiles = (
	local dir_arr = #()
	local file_arr = #()
	
	pathsToSearch = getPathsToSearch()
	
	if (rlt_Search.chk_recursiveSearching.checked) then (
		for p in pathsToSearch do
			join dir_arr (dir_arr = getDirsRecursive p)
	)

	join dir_arr pathsToSearch

	for d in dir_arr do (
		if d[d.count] != "\\" and d[d.count] != "/" then 
			d += "\\"

		try (
			local tmp_files = getFiles (d + "*.*")
			if tmp_files.count != 0 then 
				join file_arr tmp_files
		)
		catch (
			messageBox ("Could not get files from: " + d + "*.*") title:"Error" beep:true
		)	
	)
	
	file_arr
)

-- Converts a string to lowercase
fn lowercase instring = (
	local upper, lower, outstring
	upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	lower = "abcdefghijklmnopqrstuvwxyz"
	
	outstring=copy instring
	
	for i = 1 to outstring.count do (
		j = findString upper outstring[i]
		if (j != undefined) do outstring[i]=lower[j]
	)
	outstring
)

-- Finds duplicates in array A, and deletes them in both a and b
fn trim_dups a b = (
	for i in a.count to 1 by -1 do 
		(
			idx = findItem a a[i]
			if (idx != 0) AND (idx != i) do (
				deleteItem a i
				deleteItem b i
			)
		)
	a
)

-- Finds duplicates in array A, and deletes them in in A
fn trim_dupsOne a = (
	for i in a.count to 1 by -1 do 
		(
			idx = findItem a a[i]
			if (idx != 0) AND (idx != i) do 
				deleteItem a i
		)
	a
)

-- Relinking Function 
fn relinkMaps = (
	local mapfiles = #()
	local mapfilesMissing = #()
	local file_arr_filename = #()

	st = timestamp()
	local mBitmaps = getClassInstances BitmapTexture
	format "getClassInstances() completed in [% ms]
" (timestamp()-st)
	
	mapfiles = mBitmaps

	st = timestamp()	
	local file_arr = getDirectoryFiles()	-- contains the paths of found files
	format "getDirectoryFiles() completed in [% ms]
" (timestamp()-st)
	st = timestamp()
	local missingMaps = getMissingMaps()	-- Missing Map names
	format "getMissingMaps() completed in [% ms]
" (timestamp()-st)

	-- stores just the filename of the missing files in the array file_arr_filename
	for i in file_arr do
		append file_arr_filename (filenameFromPath i)

	st = timestamp()
	if (rlt_Search.chk_undoOn.checked) then ( 				-- if Undo is on
		rlt_Search.lbl_pb.caption = "Searching..."
		undo "Relink Textures" on (
			for i=1 to mapfiles.count do (
				-- for all bitmapTextures in the scene
				if (isProperty mapfiles[i] #filename) then (
					-- that have a #filename property
					if (index = findItem file_arr_filename (filenameFromPath mapfiles[i].filename)) != 0 then 	-- check if the current file is missing
						mapfiles[i].filename = file_arr[index]		-- Relinks the current file to the found file
				)
				rlt_Search.pb_bar.value = 100.*i/mapfiles.count
			)	
		)
	)
	else if not (rlt_Search.chk_undoOn.checked) then (
		rlt_Search.lbl_pb.caption = "Searching..."
		for i=1 to mapfiles.count do (
			if (isProperty mapfiles[i] #filename) then (
				if (index = findItem file_arr_filename (filenameFromPath mapfiles[i].filename)) != 0 then 		-- check if the current file is missing
					mapfiles[i].filename = file_arr[index]			-- Relinks the current file to the found file
			)
			rlt_Search.pb_bar.value = 100.*i/mapfiles.count
		)	
	)
	format "Relink Textures() completed in [% ms]
" (timestamp()-st)
)

---------------- Rollouts -------------------

rollout rlt_Main "Missing Bitmaps Window" (
	button btn_Find "Missing Bitmaps"

	on rlt_Main open do (
		clearListener()
		openRolloutNextTo rlt_Main rlt_Missing 450 405
	)
	
	on rlt_Main moved xy do 
		SetDialogPos rlt_Missing [xy.x+313, xy.y]
	
	on rlt_Main close do (
		closeRollout rlt_Missing
	)
	
	on btn_Find pressed do (
		if rlt_Missing.open then closeRollout rlt_Missing else openRolloutNextTo rlt_Main rlt_Missing 450 405
		rlt_Main.open = false
		rf.size.y = 290
	)
)

rollout rlt_Missing "Maps" (
	button btn_interactiveMode ""
	label lbl_doubleClickInfo ""
	label lbl_rightClickInfo ""
	multilistbox lst_MissingMaps "Missing Maps" width:420 height:20
	edittext edt_missingPath "" width:420
	button btnUpdate "Update"
	
	-- Updates the captions and buttons for interactive Mode based on it's current state
	-- calls UpdateMissingBitmaps()
	fn updateInteractiveMode = (

		if not interactiveModeOn then (
			btn_interactiveMode.caption = "Turn Interactive Mode ON"
			lbl_doubleClickInfo.caption = ""
			lbl_rightClickInfo.caption = "Double-click on a map to view its full path"
		)
		else (
			btn_interactiveMode.caption = "Turn Interactive Mode OFF"
			lbl_doubleClickInfo.caption = "Click on a map to select the object it is assigned to."
			lbl_rightClickInfo.caption = "Double-click on a map to isolate the object it is assigned to."
		)
		lst_MissingMaps.items = #("** LOADING MISSING BITMAPS **  This may take a moment...")
		rlt_Search.lbl_pb.caption = "LOADING BITMAPS - please wait..."
		rlt_Search.lbl_pb.caption = ""

		if (not rlt_Missing.open) then 
			rlt_Missing.open = true
		else
			rlt_Missing.updateMissingBitmaps()
	)

	on rlt_Missing open do
		updateInteractiveMode()
		
	on rlt_Missing close do (
		rlt_Main.open = true		-- rolls up and down the rollout 
		rf.size.y = 330
	)

	on btn_interactiveMode pressed do (
		interactiveModeOn = not interactiveModeOn
		updateInteractiveMode()
	)

	on btnUpdate pressed do
		rlt_Missing.updateMissingBitmaps()
	
	-- Function for updating the missing Bitmaps, does it differently if in Interactive Mode or not
	on rlt_Missing updateMissingBitmaps do (
		st = timestamp()
		if interactiveModeOn then 
			missingMaps = getMissingMapsObjs()		-- missingMaps stores what is displayed in the listbox for Missing Maps, InteractiveMode ON
		else
			missingMaps = getMissingMaps()			-- gets the missing maps in the scene faster via this method, InteractiveMode OFF
		
		format "updateMissingBitmaps - completed in [% ms]
" (timestamp()-st)
		lst_MissingMaps.items = missingMaps			-- puts it in the listbox
	)

	-- if interactiveMode is on, and the user selects a map, it will select the object it is assigned to
	on lst_MissingMaps selected arg do (
		if interactiveModeOn then (
			max create mode
			clearSelection()
			for i in lst_MissingMaps.selection do (
				local obj = missingMapsObjs[i]
				if (isValidNode obj) then
					selectmore obj	
				else													-- Scene has Changed, update
					rlt_Missing.updateMissingBitmaps()
			)
		)
	)
	
	-- if interactiveMode is off, and the user double clicks a map, it will select the object it is assigned to and isolate it
	on lst_MissingMaps doubleClicked arg do (
		if interactiveModeOn then (
			max create mode
			local obj = missingMapsObjs[arg]

			if (isValidNode obj) then (
				if Iso2Roll != undefined then 
					Iso2Roll.C2Iso.changed true -- turns off Isolation mode, if it's on
					
				macros.run "Tools" "Isolate_Selection"				
			)
			else (
				-- Scene has Changed, update
				rlt_Missing.updateMissingBitmaps()		
			)
		)
		edt_missingPath.text = lst_missingMaps.items[arg]
	)	
)

rollout rlt_Search "Search for Bitmap Paths" (
	label lbl_manualSearchLabel "Search directory:" align:#left
	edittext edt_manualSearch "" width:220 offset:[-10,0]
	button btn_browseSearch "Browse" offset:[110,-23]
	checkbox chk_recursiveSearching "Recursive File Searching" checked:true offset:[0,20]
	checkbox chk_undoOn "Undo Available" checked:false
	label lbl_undoOn "(NOTE: If Undo is Available the script will execute slower)"
	
	group "Bitmaps" (
		button btn_Relink "Relink" enabled:false
	)
	
	label lbl_pb "" align:#center offset:[0,15]
	progressbar pb_bar ""
	
	on btn_browseSearch pressed do (
		local browseDirectory = getSavePath "Search Directory" initialDir:"Q:\\_Vault\\Maps"
		if browseDirectory != undefined then (
			edt_ManualSearch.text = browseDirectory
			btn_Relink.enabled = true
		)
	)

	on btn_Relink pressed do (
		sst = timestamp()
		if (local toSearch = getPathsToSearch()) != "" then (	
			interactiveModeOn = false
			max create mode
			disablesceneredraw()
			RelinkMaps()
		
			rlt_Search.lbl_pb.caption = "Updating Missing Maps"

			enablesceneredraw()
			rlt_Missing.updateInteractiveMode()
			rlt_Search.lbl_pb.caption = ""
			rlt_Search.pb_bar.value = 0
		) else
			messageBox "Please select a search directory" beep:false
		format "Total Time: [% ms]
" (timestamp()-sst)
	)
)

----------------- Floaters ------------------
rf = newRolloutFloater "Relink Bitmaps v1.06" 300 290
addRollout rlt_Main rf rolledUp:true
addRollout rlt_Search rf

  Happy Scripting,
  Colin Senner
4 Replies

Cool, thanks for sharing!

I’m having a somewhat related problem so I hope maybe you can help.

I have to convert all bitmaps of a current project from TGA to DDS. Now the files themselves can be easily batch-processed with Photoshop and what not, but all the assignments in max are a major PITA to fix.

Is it possible to extend the script so missing maps don’t ge a new path, but a new file extension? I’m looking into your script now but don’t know how to rewrite it. This must be a piece of cake for you right? Please?

Never mind. I found a couple of other scripts which do find & replace. Too bad they don’t cover Direct3D shader maps.

hey dude great script – I just recommended it to someone in another thread!
I have found a bug though. If you close the “Maps” rollout (which appears to be named rlt_Missing in your script) before you hit the Relink button, it does everything successfully but then tries to update the maps rollout which has been closed, so causes an error. Maybe a simple flag to see whether the rollout is open or closed could do the trick.
Cheers for sharing this handy tool.

Thanks for bug testing it! I will post v1.06 here today and always keep the latest version on scriptspot! Thanks a ton, the rlt_MissingMaps fix will be included.

Cheers,
-Colin