-- @description Creates a new node graph where the -- @author me :) -- @shortcut alt + t -- @version 0.1 --------------------------------------------------------------------------------------------------- -- Configuration local labelWidth = 120 local labelHeight = 20 local editWidth = 100 local editHeight = 20 local buttonWidth = 80 local buttonHeight = 30 local guiDebug = false local settings = octane.gui.createPropertyTable() local lastSettingsFileName = octane.file.join(octane.file.getSpecialDirectories().tempDirectory, "last_texture_scatter_settings.txt") local imgFileExtensions = { ".jpg",".bmp",".ico",".jpg",".jpeg",".jng",".lbm",".mng",".pbm",".pbm",".pcd",".pcx", ".pgm",".png",".ppm",".ras",".tga",".targa",".tif",".tiff",".wbmp",".psd",".cut",".xbm", ".xpm",".dds",".gif",".hdr",".sgi",".exr",".j2k",".jp2",".pfm",".pic",".pict",".raw" } --------------------------------------------------------------------------------------------------- -- GUI -- opens a message box with an error message local function showError(text) octane.gui.showDialog{ type = octane.gui.dialogType.BUTTON_DIALOG, title = "Error", text = text } end -- creates a group with a directory edit field and a button to select a directory local function createFilePath(boundName) local editDir = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, text = "", width = 400, height = editHeight, } editDir:bind("text", settings, boundName) local btnSelectDir = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "...", width = 30, height = editHeight, callback = function(component, event) if event == octane.gui.eventType.BUTTON_CLICK then -- get and check current directory local dir = editDir.text if dir and octane.file.isAbsolute(dir) and not octane.file.isDirectory(dir) then dir = nil end -- open file chooser local ret = octane.gui.showDialog { type = octane.gui.dialogType.FILE_DIALOG, browseDirectory = true, title = "Choose the direcory containing the image files", path = dir, save = false, } -- store directory if one got chosen if ret.result ~= "" then editDir.text = ret.result end end end } return octane.gui.create { type = octane.gui.componentType.GROUP, border = false, debug = guiDebug, rows = 1, cols = 2, children = { editDir, btnSelectDir }, } end -- creates a text label and returns it local function createLabel(text, labelWidth) return octane.gui.create { type = octane.gui.componentType.LABEL, text = text, width = labelWidth, height = labelHeight, } end -- creates a numeric box and returns it local function createNumBox(value, minValue, maxValue, step, boundName) local numBox = octane.gui.create { type = octane.gui.componentType.NUMERIC_BOX, value = value, minValue = minValue, maxValue = maxValue, step = step, width = editWidth, height = editHeight, } numBox:bind("value", settings, boundName) return numBox end -- creates a group with a sub-label and 3 min/max/step fields local function createMinMaxStep(subLabel, minValue, maxValue, step, boundName) local lblSub = createLabel(subLabel, 20) local lblMin = createLabel("min", 34) local nboxMin = createNumBox(minValue, minValue, maxValue, step, boundName.."Min") local lblMax = createLabel("max", 36) local nboxMax = createNumBox(maxValue, minValue, maxValue, step, boundName.."Max") local lblStep = createLabel("step", 38) local nboxStep = createNumBox(0, 0, maxValue, step, boundName.."Step") nboxMin.callback = function(component, event) if event == octane.gui.eventType.VALUE_CHANGE and nboxMin.value > nboxMax.value then nboxMin.value = nboxMax.value end end nboxMax.callback = function(component, event) if event == octane.gui.eventType.VALUE_CHANGE and nboxMax.value < nboxMin.value then nboxMax.value = nboxMin.value end end return octane.gui.create { type = octane.gui.componentType.GROUP, text = "", border = false, centre = false, debug = guiDebug, rows = 1, cols = 7, children = { lblSub, lblMin, nboxMin, lblMax, nboxMax, lblStep, nboxStep }, } end -- groups components vertically local function createVerticalGroup(components) local count = 0 for _ in pairs(components) do count = count + 1 end return octane.gui.create { type = octane.gui.componentType.GROUP, text = "", border = false, centre = false, debug = guiDebug, rows = count, cols = 1, children = components, } end -- texture path selection local lblTexPath = createLabel("texture path:", labelWidth) local grpTexPath = createFilePath("texturePath") -- greyscale texture checkbox local lblMonoTex = createLabel("greyscale images:", labelWidth) local cboxMonoTex = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "", checked = false, width = 20, height = editHeight, } cboxMonoTex:bind("checked", settings, "greyscale") -- variation count local lblTexCount = createLabel("number variations:", labelWidth) local nboxTexCount = createNumBox(2, 2, 256, 1, "variationCount") -- random power local lblRandPower = createLabel("random power:", labelWidth) local grpRandPower = createMinMaxStep("", 0, 1, 0.001, "randPower") -- random gamma local lblRandGamma = createLabel("random gamma:", labelWidth) local grpRandGamma = createMinMaxStep("", 0.001, 8, 0.001, "randGamma") -- random rotation local lblRandRot = createLabel("random rotation:", labelWidth) local grpRandRot = createMinMaxStep("", 0, 360, 0.001, "randRot") -- scale local lblRandScale = createLabel("random scale:", labelWidth) local grpRandScale = createVerticalGroup({ createMinMaxStep("U", 0.001, 1000, 0.001, "randScaleU"), createMinMaxStep("V", 0.001, 1000, 0.001, "randScaleV") }) -- translation local lblRandTrans = createLabel("random translation:", labelWidth) local grpRandTrans = createVerticalGroup({ createMinMaxStep("U", -1000, 1000, 0.001, "randTransU"), createMinMaxStep("V", -1000, 1000, 0.001, "randTransV") }) -- settings layout group local grpSettings = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", border = false, inset = { 4 }, padding = { 2, 4 }, centre = false, debug = guiDebug, rows = 8, cols = 2, children = { lblTexPath, grpTexPath, lblMonoTex, cboxMonoTex, lblTexCount, nboxTexCount, lblRandPower, grpRandPower, lblRandGamma, grpRandGamma, lblRandRot, grpRandRot, lblRandScale, grpRandScale, lblRandTrans, grpRandTrans, }, } -- button layout group local btnOk = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "OK", width = buttonWidth, height = buttonHeight, } local btnCancel = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Cancel", width = buttonWidth, height = buttonHeight, } local grpButtons = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", border = false, padding = { 0 }, centre = true, -- width = grpSettings.width, debug = guiDebug, rows = 1, cols = 2, children = { btnOk, btnCancel }, } grpButtons.padding = { (grpSettings.width - grpButtons.width) / 4, 10 } -- window that holds all components local grpMain = createVerticalGroup({grpSettings, grpButtons}) local winSettings = octane.gui.create { type = octane.gui.componentType.WINDOW, text = "Texture Scatter", children = { grpMain }, width = grpMain.width, height = grpMain.height, } --------------------------------------------------------------------------------------------------- -- Loading/saving settings -- the defaults we use in case of no stored settings settings.texturePath = "" settings.variationCount = 10 settings.greyscale = false settings.randPowerMin = 0.5 settings.randPowerMax = 1 settings.randPowerStep = 0 settings.randGammaMin = 2.2 settings.randGammaMax = 2.2 settings.randGammaStep = 0 settings.randRotMin = 0 settings.randRotMax = 360 settings.randRotStep = 90 settings.randScaleUMin = 1 settings.randScaleUMax = 1 settings.randScaleUStep = 0 settings.randScaleVMin = 1 settings.randScaleVMax = 1 settings.randScaleVStep = 0 settings.randTransUMin = 0 settings.randTransUMax = 0 settings.randTransUStep = 0 settings.randTransVMin = 0 settings.randTransVMax = 0 settings.randTransVStep = 0 -- loads settings from a file local function loadSettings(fileName) -- open file file, errorMsg = io.open(fileName, "r") if not file then showError("Could not open file '"..fileName.."'. Error:\n"..errorMsg) return end -- read all entries for line in file:lines() do local splitPos = line:find(" ") if splitPos then local key = line:sub(1, splitPos-1) local value = line:sub(splitPos+1) local valueType = type(settings[key]) if valueType == "number" then settings[key] = tonumber(value) elseif valueType == "boolean" then if value == "true" then settings[key] = true elseif value == "false" then settings[key] = false end elseif valueType == "string" then settings[key] = value end end end -- close file file:close() end -- saves settings into a file local function saveSettings(fileName) -- open file file, errorMsg = io.open(fileName, "w") if not file then showError("Could not open file '"..fileName.."'. Error:\n"..errorMsg) return end -- store all entries from the shadow table of the property table for key,value in pairs(settings["__octane.shadow"]) do file:write(key.." "..tostring(value).."\n") end -- close file file:close() end -- load if octane.file.exists(lastSettingsFileName) and not octane.file.isDirectory(lastSettingsFileName) then loadSettings(lastSettingsFileName) end --------------------------------------------------------------------------------------------------- -- Texture scattering -- helper to calculate a random value for a specific min/max/step setting local function calcRandom(propName) -- fetch settings for property local minV = settings[propName.."Min"] local delta = settings[propName.."Max"] - minV local step = settings[propName.."Step"] -- return quantized value if required if step > 0 then return minV + math.floor(math.random() * (delta+step) / step) * step -- otherwise just something in [min..max) else return minV + math.random()*delta end end -- the actual function doing the work local function createScatterTextures() -- save current settings saveSettings(lastSettingsFileName) -- check directory if not settings.texturePath or not octane.file.isAbsolute(settings.texturePath) or not octane.file.isDirectory(settings.texturePath) then showError("Invalid texture director '"..settings.texturePath.."'") return false end -- build fast look up table of image extensions local imgExtMap = {} for _,ext in ipairs(imgFileExtensions) do imgExtMap[ext] = true end -- gather all image file names local allFiles = octane.file.listDirectory(settings.texturePath, true, false, true, false) local imageFileNames = {} for _,file in ipairs(allFiles) do if imgExtMap[octane.file.getFileExtension(file):lower()] then table.insert(imageFileNames, file) end end -- dump error and return if there are no image files if #imageFileNames == 0 then showError("No image files found in '"..settings.texturePath.."'") return false end -- get destination node graph (either root or the parent graph of first selected item) local destGraph = octane.project.getSceneGraph() local destPos = { 400, 200 } local selected = octane.project.getSelection()[1] if selected and selected.graphOwned then destGraph = selected.graphOwner destPos = selected.position end -- create node graph with seed input, random color texture, gradient and texture output local graph = octane.nodegraph.create { type = octane.GT_STANDARD, graphOwner = destGraph, name = "Scattered Texture", position = destPos, } local seedInputLinkerNode = octane.node.create { type = octane.NT_IN_INT, name = "Seed", graphOwner = graph, } local seedInputNode = octane.node.create { type = octane.NT_INT, pinOwnerNode = seedInputLinkerNode, pinOwnerId = octane.P_INPUT, } local randomTextureNode = octane.node.create { type = octane.NT_TEX_RANDOMCOLOR, name = "Random", graphOwner = graph, } local gradientTextureNode = octane.node.create { type = octane.NT_TEX_GRADIENT, name = "Gradient", graphOwner = graph, } local texOutputLinkerNode = octane.node.create { type = octane.NT_OUT_TEXTURE, name = "Texture", graphOwner = graph, } -- connect the nodes randomTextureNode:connectTo(octane.P_RANDOM_SEED, seedInputLinkerNode) gradientTextureNode:connectTo(octane.P_INPUT, randomTextureNode) texOutputLinkerNode:connectTo(octane.P_INPUT, gradientTextureNode) -- create enough entries in the gradient texture node to fit settings.variationCount textrues gradientTextureNode:setAttribute(octane.A_NUM_CONTROLPOINTS, settings.variationCount-2, true) -- seed the RNG math.randomseed(os.time()) for i=1,50 do math.random() end -- create the image textures as internal nodes of the gradient node and set the gradient positions local imgTexNodeType = settings.greyscale and octane.NT_TEX_FLOATIMAGE or octane.NT_TEX_IMAGE local imgTexNodeName = octane.apiinfo.getNodeInfo(imgTexNodeType).defaultName local posNodeName = octane.apiinfo.getNodeInfo(octane.NT_FLOAT).defaultName for i=1,settings.variationCount do -- create the image texture node ... local imgTexNode if i < 3 then -- ... for the min/max pins imgTexNode = octane.node.create { type = imgTexNodeType, pinOwnerNode = gradientTextureNode, pinOwnerId = (i == 1) and octane.P_MIN or octane.P_MAX, name = imgTexNodeName, } else -- ... for the dynamic (inbetween) pins local pinIx = gradientTextureNode:getNodeInfo().staticPinCount + (i-2) * 2 imgTexNode = octane.node.create { type = imgTexNodeType, pinOwnerNode = gradientTextureNode, pinOwnerIx = pinIx, name = imgTexNodeName, } -- also create a position node and set its value local posNode = octane.node.create { type = octane.NT_FLOAT, pinOwnerNode = gradientTextureNode, pinOwnerIx = pinIx - 1, name = posNodeName, } posNode:setAttribute(octane.A_VALUE, (i-2) / (settings.variationCount-1)) end -- set up image texture power and gamme imgTexNode:setPinValue(octane.P_POWER, calcRandom("randPower")) imgTexNode:setPinValue(octane.P_GAMMA, calcRandom("randGamma")) -- set up image texture UV transformation local transNode = imgTexNode:getConnectedNode(octane.P_TRANSFORM) transNode:setAttribute(octane.A_ROTATION, { 0, 0, calcRandom("randRot") }, false) local vec = transNode:getAttribute(octane.A_ROTATION) transNode:setAttribute(octane.A_SCALE, { calcRandom("randScaleU"), calcRandom("randScaleV") }, false) transNode:setAttribute(octane.A_TRANSLATION, { calcRandom("randTransU"), calcRandom("randTransV") }, false) transNode:evaluate() -- set file name imgTexNode:setAttribute(octane.A_FILENAME, imageFileNames[math.random(1, #imageFileNames)]) end -- unfold the whole mess and return graph:unfold(true) return true end --------------------------------------------------------------------------------------------------- -- Main -- hook up functions to buttons btnOk.callback = function() if createScatterTextures() then winSettings:closeWindow() end end btnCancel.callback = function() winSettings:closeWindow() end -- open window which will block until finished winSettings:showWindow()