-- ideally these would all come from input pins local settings = {} settings.greyscale = false settings.interpType = octane.gradientInterpType.CONSTANT 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 = 45 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 -- 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 local function randomFlip(propName) if (settings[propName]) then return math.random(0, 1) * 2 - 1 end return 1 end local ScatterScript = {} local inputs, texOutputLinkerNode, randomTextureNode, gradientTextureNode -- we should destroy some nodes on refreshing: local otherNodes = {} -- the actual function doing the work -- if graph is nil, a new graph is created, otherwise the existing one is updated function ScatterScript.createScatterTextures(self, graph) -- get inputs settings.variationCount = math.max(2, self:getInputValue(inputs[3])) local textureList = self:getInputValue(inputs[2]) -- split lines: settings.textures = {} for s in textureList:gmatch("[^\r\n]+") do table.insert(settings.textures, s) end if #settings.textures == 0 then return end -- check textures if not next(settings.textures) then showError("Please select image files first.") return false end -- interpolation type createExtraEntry = (settings.interpType == octane.gradientInterpType.CONSTANT) gradientTextureNode:setPinValue(octane.P_GRADIENT_INTERP_TYPE, settings.interpType, true) -- create enough entries in the gradient texture node to fit settings.variationCount textrues local controlPointCount = settings.variationCount-2 if createExtraEntry then controlPointCount = controlPointCount + 1 end gradientTextureNode:setAttribute(octane.A_NUM_CONTROLPOINTS, controlPointCount, true) -- seed the RNG math.randomseed(os.time()) for i=1,50 do math.random() end -- delete previous image textures: for _, v in ipairs(otherNodes) do v:destroy() end otherNodes = {} -- 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 -- Static pin count is not yet exposed through the API: local gradientStaticPinCount = gradientTextureNode.pinCount - 2 * controlPointCount for i=1,settings.variationCount do -- create the image texture node ... local imgTexNode if i == 1 or i == controlPointCount + 2 then -- ... for the min/max pins imgTexNode = octane.node.create { type = imgTexNodeType, graphOwner = graph, name = imgTexNodeName, } table.insert(otherNodes, imgTexNode) if i == 1 then gradientTextureNode:connectTo(octane.P_MIN, imgTexNode) end if i ~= 1 or createExtraEntry then -- If we have an extra entry, create one image texture to both -- "min" and "max" input pins -- If not, then the last node is connected to the "max" pin gradientTextureNode:connectTo(octane.P_MAX, imgTexNode) end else -- ... for the dynamic (inbetween) pins local pinIx = gradientStaticPinCount + (i-1) * 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-1) / (controlPointCount + 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) local scale = {0, calcRandom("randScaleV")} if settings.randScaleKeepRatio then scale[1] = scale[2] * settings.randScaleRatio else scale[1] = calcRandom("randScaleU") end transNode:setAttribute(octane.A_SCALE, { scale[1] * randomFlip("randMirrorU"), scale[2] * randomFlip("randMirrorV") }, false) transNode:setAttribute(octane.A_TRANSLATION, { calcRandom("randTransU"), calcRandom("randTransV") }, false) transNode:evaluate() -- set file name imgTexNode:setAttribute(octane.A_FILENAME, settings.textures[math.random(1, #settings.textures)]) end -- unfold the whole mess and return graph:unfold(true) selectedGraph = graph return true end --------------------- -- scripted graph -- onInit function, this is called once in the beginning. function ScatterScript.onInit(self, graph) inputs = graph:setInputLinkers { { type = octane.PT_INT, label = "Seed", defaultNodeType=octane.NT_INT, fromNodeType=octane.NT_TEX_RANDOMCOLOR, fromPinId=octane.P_RANDOM_SEED }, { type = octane.PT_STRING, label = "File list", defaultNodeType=octane.NT_STRING, defaultValue="", multiLine=true}, { type = octane.PT_INT, label = "Variation count", defaultNodeType=octane.NT_INT, defaultValue = 15, bounds={2, 100} }, } texOutputLinkerNode = unpack(graph:setOutputLinkers { { type = octane.PT_TEXTURE, label = "Texture" }, }) -- create default node in our input linker if not inputs[1]:getConnectedNodeIx(1) then octane.node.create { type = octane.NT_INT, pinOwnerNode = inputs[1], pinOwnerId = octane.P_INPUT, } end randomTextureNode = octane.node.create { type = octane.NT_TEX_RANDOMCOLOR, name = "Random", graphOwner = graph, } gradientTextureNode = octane.node.create { type = octane.NT_TEX_GRADIENT, name = "Gradient", graphOwner = graph, } -- connect the nodes randomTextureNode:connectTo(octane.P_RANDOM_SEED, inputs[1]) gradientTextureNode:connectTo(octane.P_INPUT, randomTextureNode) texOutputLinkerNode:connectTo(octane.P_INPUT, gradientTextureNode) self:createScatterTextures(graph) end -- this function is called every time the value of an input linker changes function ScatterScript.onEvaluate(self, graph) local changed = false for i = 2, #inputs do changed = changed or self:inputWasChanged(inputs[i]) end if changed then self:createScatterTextures(graph) end end -- default name ScatterScript._name = "Random texture scatter" -- In lua, a script is run as if the script defines a function body, so -- like in a function you may return a value return ScatterScript