-- 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.30" -------------------------------------------------------- -- 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 (change if state with albedoTex) --if resDif.result ~= "" then if string.find(resDif.result, "_albedo") ~= nil then fileEditorA:updateProperties{ text = resDif.result } INA_PATH = resDif.result createButton:updateProperties{ enable = true } else showError("The Albedo suffix is different from settings", halt) createButton:updateProperties{ enable = false } fileEditorA:updateProperties{ text = "" } INA_PATH = nil end print("Loading Albedo Map:\t\t\t", octane.file.exists(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 Roughness Map:\t\t", octane.file.exists(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 Normal Map:\t\t\t", octane.file.exists(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 Metalness Map:\t\t", octane.file.exists(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 Displacement Map:\t", octane.file.exists(IND_PATH), "\n") -- 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!\n") 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() -- function to output Power and Gamma from Maps local function outPowerGamma (node, pos) prop = node:getProperties() pOut = octane.node.create { type = octane.NT_IN_TEXTURE, name = string.gsub(node.name, "Map", "power"), graphOwner = node:getProperties().graphOwner, position = {pos, 0} } outP = octane.node.create { type = octane.NT_TEX_FLOAT, name = string.gsub(node.name, "Map", "power"), pinOwnerNode = pOut, pinOwnerId = octane.P_INPUT } outP:setAttribute(octane.A_VALUE, 1, true) node:connectTo(octane.P_POWER, pOut) gOut = octane.node.create { type = octane.NT_IN_FLOAT, name = string.gsub(node.name, "Map", "gamma"), graphOwner = node:getProperties().graphOwner, position = {pos+50, 0} } outG = octane.node.create { type = octane.NT_FLOAT, name = string.gsub(node.name, "Map", "gamma"), pinOwnerNode = gOut, pinOwnerId = octane.P_INPUT } outG:setAttribute(octane.A_VALUE, 2.2, true) node:connectTo(octane.P_GAMMA, gOut) end -- 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", graphOwner = root} Color1:setAttribute(octane.A_FILENAME, INA_PATH) destinationsA = AlbNode[1]:getDestinationNodes() for i, v in pairs (destinationsA) do destinationsA[i].node:connectTo(destinationsA[i].pin, Color1) end AlbNode[1]:destroy() outPowerGamma(Color1, 0) -- 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", graphOwner = root} Color2:setAttribute(octane.A_FILENAME, INN_PATH) destinationsN = NormNode[1]:getDestinationNodes() for i, v in pairs (destinationsN) do destinationsN[i].node:connectTo(destinationsN[i].pin, Color2) end NormNode[1]:destroy() outPowerGamma(Color2, 600) -- 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", graphOwner = root} Color3:setAttribute(octane.A_FILENAME, INR_PATH) destinationsR = RougNode[1]:getDestinationNodes() for i, v in pairs (destinationsR) do destinationsR[i].node:connectTo(destinationsR[i].pin, Color3) end RougNode[1]:destroy() outPowerGamma(Color3, 200) -- 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", graphOwner = root} Color4:setAttribute(octane.A_FILENAME, INM_PATH) destinationsM = MetalNode[1]:getDestinationNodes() for i, v in pairs (destinationsM) do destinationsM[i].node:connectTo(destinationsM[i].pin, Color4) end MetalNode[1]:destroy() outPowerGamma(Color4, 400) -- correct the Gamma value for Roughness, Normal and Metalness --Color2:setPinValue(octane.P_GAMMA, 1, true) --Normal Gamma should be 2.2? Color3:setPinValue(octane.P_GAMMA, 1, true) Color4:setPinValue(octane.P_GAMMA, 1, true) -- creates Dielectric node DielbNode = root:findItemsByName("Dielectric F0 Reflection", false) Diel = octane.node.create { type = octane.NT_IN_TEXTURE, name = "Dielectric F0 Reflection", graphOwner = root, position = {700, 0} } DielV = octane.node.create { type = octane.NT_TEX_FLOAT, pinOwnerNode = Diel, pinOwnerId = octane.P_INPUT } DielV:setAttribute(octane.A_VALUE, 0.040) destinationsE = DielbNode[1]:getDestinationNodes() for i, v in pairs (destinationsE) do destinationsE[i].node:connectTo(destinationsE[i].pin, Diel) end DielbNode[1]:destroy() -- add projection and uv transform to bitmap nodes prj = octane.node.create { type = octane.NT_IN_PROJECTION, name = "Projection", graphOwner = root, position = {1000, 0} } prjM = octane.node.create { type = octane.NT_PROJ_UVW, name = "Mesh UV", pinOwnerNode = prj, pinOwnerId = octane.P_INPUT } Color1:connectTo(octane.P_PROJECTION, prj) Color2:connectTo(octane.P_PROJECTION, prj) Color3:connectTo(octane.P_PROJECTION, prj) Color4:connectTo(octane.P_PROJECTION, prj) Tuv = octane.node.create { type = octane.NT_IN_TRANSFORM, name = "UV transform", graphOwner = root, position = {1100, 0} } Tras = octane.node.create { type = octane.NT_TRANSFORM_2D, name = "2D transform", pinOwnerNode = Tuv, pinOwnerId = octane.P_INPUT } Color1:connectTo(octane.P_TRANSFORM, Tuv) Color2:connectTo(octane.P_TRANSFORM, Tuv) Color3:connectTo(octane.P_TRANSFORM, Tuv) Color4:connectTo(octane.P_TRANSFORM, Tuv) -- 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", graphOwner = root} Color5:setAttribute(octane.A_FILENAME, IND_PATH) destinationsD = DispT:getDestinationNodes() for i, v in pairs (destinationsD) do destinationsD[i].node:connectTo(destinationsD[i].pin, Color5) end DispT:destroy() DispH = octane.node.create{type= octane.NT_IN_FLOAT, name="Displacement Height", graphOwner = root, position = {800, 0}} dispValue= octane.node.create{type= octane.NT_FLOAT, name="DisplacementHeight", pinOwnerNode=DispH, pinOwnerId=octane.P_INPUT} dispValue:setAttribute(octane.A_VALUE, 0.001) DispNode[1]:connectTo(octane.P_AMOUNT, DispH) DispM = octane.node.create{type= octane.NT_IN_FLOAT, name="Mid Level", graphOwner = root, position = {900, 0}} dispMed = octane.node.create{type= octane.NT_FLOAT, name="Mid", pinOwnerNode=DispM, pinOwnerId=octane.P_INPUT} dispMed:setAttribute(octane.A_VALUE, 0.5) DispNode[1]:connectTo(octane.P_BLACK_LEVEL, DispM) DispLOD = DispNode[1]:getConnectedNode(octane.P_LEVEL_OF_DETAIL) DispLOD:setAttribute(octane.A_VALUE, 12, true) Color5:connectTo(octane.P_PROJECTION, prj) Color5:connectTo(octane.P_TRANSFORM, Tuv) 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 ("PBR material", fileName, "succesfully created\n") root : updateProperties{name=fileName} -- reorganize all the nodes in the graph pbrOut = root:findItemsByName("PBRMaterial", false) pbrOut[1]:deleteUnconnectedItems() 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("%s.orbx file already exist", fileName)) end else print("Goodbye") end