-- Create a material to use with Quixel PBR. -- @description Create a MMB QUIXEL PBR mateial -- The script searches and downloads the MBB_QUIXEL_PBR.orbx material provided by miko3d, -- asks for the albedo texture path and then automatically imports all the needed maps in the proper pins, -- and then the resulting material will be saved as an .orbx file if needed. -- @author Beppe Gullotta aka "bepeg4d" local version = "0.26" -------------------------------------------------------- -- global variable that holds the maps suffix; change the suffix and save the script if you use a different pattern albedoTex = "_albedo" fileTextN = "_normal" fileTextR = "_roughness" fileTextM = "_metalness" fileTextD = "_displacement" saveORBX = true -- true or false: if true, the resulting material will be saved as an .orbx file -- GUI code -------------------------------------------- -- creates a note text and returns it local function createLabel(text) return octane.gui.create { type = octane.gui.componentType.LABEL, text = text, width = 500, height = 24, } end -- lets create a note label local noteLbl1 = createLabel("Choose the path for the Albedo map to automatically create a MMB QUIXEL PBR material") -- for layouting the notes we use a group local noteGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 1, cols = 1, children = { noteLbl1, }, padding = { 2 }, inset = { 5 }, } -- helper to pop-up an error dialog and optionally halts the script local function showError(text, halt) octane.gui.showDialog { type = octane.gui.dialogType.BUTTON_DIALOG, title = "Create MMB QUIXEL PBR Material Error", text = text, icon = octane.gui.dialogIcon.WARNING, } if halt then error("ERROR: "..text) end end -- ALBEDO -- -- create a button to show a file chooser for ALBEDO Map local fileChooseButtonA = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Choose path", width = 80, height = 20, } -- create an editor that will show the chosen file path for Albedo Map local fileEditorA = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, text = "", x = 20, width = 305, height = 20, enable = true, } -- create a label local noteLblA1 = octane.gui.create { type = octane.gui.componentType.LABEL, text = "suffix", width = 20, } -- create a text field for the suffix to add at the end of the name of the material local fileTextA = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, text = "", width = 60, height = 20, enable = true, } -- for layouting all the elements for the Albedo Map we use a group local albGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "Albedo Map", rows = 1, cols = 4, children = { fileChooseButtonA, fileEditorA, noteLblA1, fileTextA, }, padding = { 2 }, inset = { 5 }, } -- ORBX SAVING -- create a check box for the ORBX saving option local checkBoxO1 = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, checked = saveORBX, text = "Save the resulting MMB QUIXEL PBR material as an .orbx file", width = 500, } -- for layouting all the elements for the ORBX check box we use a group local orbxGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "ORBX saving", rows = 1, cols = 1, children = { checkBoxO1, }, padding = { 2 }, inset = { 5 }, } -- BUTTONS -- local createButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Create", width = 160, height = 20, } local exitButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Exit", width = 160, height = 20, } local buttonGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 1, cols = 2, children = { createButton, exitButton, }, padding = { 5 }, border = false, } -- group that layouts the other groups local layoutGrp = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 4, cols = 1, children = { noteGrp, albGrp, orbxGrp, buttonGrp, }, centre = true, padding = { 2 }, border = false, debug = false, -- true to show the outlines of the group, handy } -- window that holds all components local MixMatWindow = octane.gui.create { type = octane.gui.componentType.WINDOW, text = "Create a MMB QUIXEL PBR Material v "..version, children = { layoutGrp }, width = layoutGrp:getProperties().width, height = layoutGrp:getProperties().height, } -- END GUI CODE ----------------------------------------- -- enable all the ui fileChooseButtonA :updateProperties{ enable = true} fileTextA :updateProperties{ enable = true} checkBoxO1 :updateProperties{ enable = true} createButton :updateProperties{ enable = false} exitButton :updateProperties{ enable = true} --button functions local function createGraph() MixMatWindow:closeWindow(true) end local function exitAndDontCreateGraph() MixMatWindow:closeWindow(false) end -- global variable that holds the input path INA_PATH = nil INN_PATH = nil INR_PATH = nil INM_PATH = nil IND_PATH = nil -- callback handling the GUI elements local function guiCallback(component, event) -- FILE CHOOSING -- if component == fileChooseButtonA then -- choose an input RGB file local resDif = octane.gui.showDialog { type = octane.gui.dialogType.FILE_DIALOG, title = "Choose the image texture for the Albedo channel", wildcards = "*.jpg; *.png; *.psd; *.tif; *.tga", save = false } -- if a file is chosen for the Albedo channel if resDif.result ~= "" then fileEditorA:updateProperties{ text = resDif.result } INA_PATH = resDif.result createButton:updateProperties{ enable = true } else createButton:updateProperties{ enable = false } fileEditorA:updateProperties{ text = "" } INA_PATH = nil end print("Loading A Map: ", INA_PATH) -- if a file is chosen, search for the Roughness Map if resDif.result ~= "" then pathD = octane.file.getParentDirectory(INA_PATH) pathR = octane.file.getFileName(INA_PATH) pathR = pathR.gsub(pathR, albedoTex, fileTextR) INR_PATH = (pathD.."/"..pathR) end print("Loading R Map: ", INR_PATH) -- if a file is chosen, search for the Normal Map if resDif.result ~= "" then pathB = octane.file.getFileName(INA_PATH) pathB = pathB.gsub(pathB, albedoTex, fileTextN) INN_PATH = (pathD.."/"..pathB) end print("Loading N Map: ", INN_PATH) -- if a file is chosen, search for the Metalness Map if resDif.result ~= "" then pathB = octane.file.getFileName(INA_PATH) pathB = pathB.gsub(pathB, albedoTex, fileTextM) INM_PATH = (pathD.."/"..pathB) end print("Loading M Map: ", INM_PATH) -- if a file is chosen, search for the Displacement Map if resDif.result ~= "" then pathB = octane.file.getFileName(INA_PATH) pathB = pathB.gsub(pathB, albedoTex, fileTextD) IND_PATH = (pathD.."/"..pathB) end print("Loading D Map: ", IND_PATH) -- close -- elseif component == createButton then createGraph() elseif component == exitButton then exitAndDontCreateGraph() elseif component == MixMatWindow then -- when the window closes, print something if event == octane.gui.eventType.WINDOW_CLOSE then print ("Let's go!") end end end -- hookup the callback with all the GUI elements fileChooseButtonA :updateProperties { callback = guiCallback } fileTextA :updateProperties { callback = guiCallback } checkBoxO1 :updateProperties { callback = guiCallback } createButton :updateProperties { callback = guiCallback } exitButton :updateProperties { callback = guiCallback } MixMatWindow :updateProperties { callback = guiCallback } -- the script will block here until the window closes local create = MixMatWindow:showWindow() -- stop the script if the user clicked the exit button if not create then return end -- creates the "macro" node graph, ie. root for all other nodes params = {} params.type = octane.GT_STANDARD params.name = "PBR-mat" params.position = {100, 100} root = octane.nodegraph.create(params) -- copy the MBB_QUIXEL_PBR material into the new Graph output = octane.livedb.downloadMaterial(1179, root) if output == nil then error("Please connect to the internet to download the MBB_QUIXEL_PBR preset") end copy = root:copyItemTree(output) oldMBB = root:findItemsByName("MBB_QUIXEL_PBR", false) oldMBB[1]:destroy() -- creates an image Map node for Albedo channel AlbNode = root:findItemsByName("Albedo", false) Color1 = octane.node.create{type= octane.NT_TEX_IMAGE, name="Albedo_Map", pinOwnerNode=AlbNode[1], pinOwnerId=octane.P_INPUT} Color1:setAttribute(octane.A_FILENAME, INA_PATH) -- creates an image Map node for Normal channel NormNode = root:findItemsByName("NormalMap", false) Color2 = octane.node.create{type= octane.NT_TEX_IMAGE, name="Normal_Map", pinOwnerNode=NormNode[1], pinOwnerId=octane.P_INPUT} Color2:setAttribute(octane.A_FILENAME, INN_PATH) -- creates an image Map node for Roughness channel RougNode = root:findItemsByName("Roughness", false) Color3 = octane.node.create{type= octane.NT_TEX_IMAGE, name="Roughness_Map", pinOwnerNode=RougNode[1], pinOwnerId=octane.P_INPUT} Color3:setAttribute(octane.A_FILENAME, INR_PATH) -- creates an image Map node for Metalness channel MetalNode = root:findItemsByName("Metalness", false) Color4 = octane.node.create{type= octane.NT_TEX_IMAGE, name="Metalness_Map", pinOwnerNode=MetalNode[1], pinOwnerId=octane.P_INPUT} Color4:setAttribute(octane.A_FILENAME, INM_PATH) -- creates an image Map node for Displacement channel if needed dispMap = octane.file.exists(IND_PATH) if dispMap == true then DispNode = root:findNodes(octane.NT_DISPLACEMENT, false) DispT = DispNode[1]:getConnectedNode(octane.P_TEXTURE) Color5 = octane.node.create{type= octane.NT_TEX_FLOATIMAGE, name="Displacement_Map", pinOwnerNode=DispT, pinOwnerId=octane.P_INPUT} Color5:setAttribute(octane.A_FILENAME, IND_PATH) DispH = root:findItemsByName("DisplacementHeight", false) dispValue= octane.node.create{type= octane.NT_FLOAT, name="DisplacementHeight", pinOwnerNode=DispH[1], pinOwnerId=octane.P_INPUT} dispValue:setAttribute(octane.A_VALUE, 0.001) end -- set the name of the material as the name of the Map and add the suffix fileName = octane.file.getFileNameWithoutExtension(INA_PATH) print (fileName) fileName= fileName.gsub(fileName, '_.+', "") fileName=(fileName .. fileTextA.text) print (fileName) root : updateProperties{name=fileName} -- reorganize all the nodes in the graph octane.nodegraph.unfold(root, false) -- (optional) save the material as .orbx file print(checkBoxO1.checked) if checkBoxO1.checked == true then pathOB = string.format("%s/%s.orbx", octane.file.getParentDirectory(INA_PATH), fileName) if octane.file.exists(pathOB) ~= true then rootgraph = octane.nodegraph.createRootGraph(fileName) sourceItems = {root} sourceCopies = rootgraph:copyFrom(sourceItems) octane.nodegraph.exportToFile(rootgraph, pathOB) print(string.format("saved %s.orbx in %s", fileName, octane.file.getParentDirectory(INA_PATH))) else print (string.format("The %s.orbx file already exist", fileName)) end else print("Goodbye") end