[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
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