Today we'll give a brief overview of the node and graph module. Every Octane user is familiar with nodes and graphs but let's explain some details to get you up to speed. We won't build a full example here but we'll include some code snippets to experiment with. Note: not all the code here will work with version 1.21, some stuff was added in 1.22.
Before we take a deep dive, let's have a look at the octane module. This module contains common enumerations used in the node and graph modules:
- octane.attributeId: A table with all the attribute identifiers available in Octane. (prefix A_ e.g. A_VALUE).
- octane.attributeType: A table with all the attribute types (prefix AT_ e.g. AT_BYTE).
- octane.graphType: A table with all the graph types (prefix GT_ e.g. GT_STANDARD).
- octane.nodeType: A table with all the node types (prefix NT_ e.g. NT_CAM_THINLENS).
- octane.pinId: A table with all the pin identifiers (prefix P_ e.g. P_KERNEL).
- octane.pinType: A table with all the pin types (prefix PT_ e.g. PT_TEX).
When manipulating the node system, you'll be using above enums all the time. That's why we made them available directly in the octane table to save some typing (e.g. octane.nodeType.NT_BOOL is equivalent with octane.NT_BOOL).
The following code will list all nodes available. Note that all not all nodes can be created, some are internal to Octane (we probably hide them in the future):
- Code: Select all
-- list all the node types
for type, value in pairs(octane.nodeType) do
print(type, value)
end
To avoid confusion, often we'll talk of items. An item can either be a node or a graph. We usally talk of items when refering to properties that apply both to node or graphs (e.g. attributes, owners, ...).
Nodes can be created from a script and they are added to the current project. We can create nodes either in a graph or internal in a pin (graph owned or pin owned). Internal nodes must have the same output type as the type of the pin they're created in and they are implicitely connected. For example:
- Code: Select all
-- create a node in the root graph (a.k.a the scene graph)
rt = octane.node.create{ type=octane.NT_RENDERTARGET, name="Impl RT" } -- implicit in the root graph
bool = octane.node.create{ type=octane.NT_BOOL, name="Expl Bool", graphOwner=octane.nodegraph.getRootGraph() }
print(rt:getProperties().graphOwned, rt:getProperties().graphOwner)
-- create a panoramic camera in the camera pin of the render target
-- we need to specify both the node that has the pin and the pin itself
pcam = octane.node.create{ type=octane.NT_CAM_PANORAMIC, pinOwnerNode=rt, pinOwnerId=octane.P_CAMERA }
print(pcam:getProperties().pinOwned, pcam:getProperties().pinOwnerNode)
-- creating a bool node in the kernel pin of the render target won't work
-- there should be an error in the log
octane.node.create{ type=octane.NT_BOOL, name="Won't work", pinOwnerNode=rt, pinOwnerId=octane.P_KERNEL }
Both nodes and graphs can have attributes. Attributes represent an internal value of the item that's not exposed to the outside world. For example a float node needs to keep around it's value. This is done via its attribute (octane.A_VALUE). When setting the value of an attribute on an item, the item needs to be evaluated. Evalution of an item means that the item checks which attributes changed and does something special with them. The specific "magic" is different per node. Evalution of nodes that aren't set up correctly can crash Octane. The dangerous ones are the image textures and the meshes. For example when changing the A_FILENAME attribute in an image texture will load the texture from disk and set up the remaining attributes. All the attributes can be accessed via their id, name of index. There are always 2 flavours of attribute manipulation functions. One function to access via index and the other to access via name or id (e.g. getAttribute vs getAttributeIx, setAttribute vs setAttributeIx). Functions taking an index always end in Ix. When manipulating attributes it's recommended to always identify attributes via it's id. This leads to more readable and less error prone code. Ideally you should only access attributes via their index when iterating over all the attributes.
- Code: Select all
-- create a bool node and get it's attribute value
boolNode = octane.node.create{ type=octane.NT_BOOL, name="My Bool" }
print(boolNode:getAttribute(octane.A_VALUE))
-- set the value of the attribute to true
boolNode:setAttribute(octane.A_VALUE, true)
print(boolNode:getAttribute(octane.A_VALUE))
-- create an image texture
texNode = octane.node.create{ type=octane.NT_TEX_IMAGE, name="Tex" }
-- none of the attributes on the node are set
print("before eval")
print(texNode:getAttribute(octane.A_TYPE))
print(texNode:getAttribute(octane.A_SOURCE_INFO))
-- set up the filename in the texture (MODIFY THIS TO SOME of your own textures)
-- on evaluation, Octane will try to load the texture and fill in the empty attributes
texNode:setAttribute(octane.A_FILENAME, "/home/thomas/Documents/textures/uv_test.jpg")
print("after eval")
print(texNode:getAttribute(octane.A_TYPE))
print(texNode:getAttribute(octane.A_SOURCE_INFO))
print(texNode:getAttribute(octane.A_SIZE)[1], texNode:getAttribute(octane.A_SIZE)[2])
Most nodes have input pins (graphs don't have input pins). A connection can be made with another node if the output type of the other node matches the pin type. When creating an internal node the connection is made implicitely. When a connection can't be made, the script will halt with an error. Like with attributes, pins can be accessed via id, name or index. Using the id is the preferred method. Pins can have a specific value range. For example fov on the camera goes from 1 to 179 degrees. When you connect a float node with x value 1000 to the camera's fov pin, the fov will still be only 179 and not 1000. This is because pins validate the value they get from a connected node. If you want modify or fetch the value of a connected node, you can either do it directly via the node (setAttribute, getAttribute) or "through" the pin (setPinValue, getPinValue). When going through the pin, the value is clamped to the range of the pin. (pins that have this behavior are PT_FLOAT, PT_INT, PT_ENUN and PT_TRANSFORM).
- Code: Select all
-- create a diffuse material
mat = octane.node.create{ type=octane.NT_MAT_DIFFUSE, name="Diffuse", position={ 500, 500 } }
-- let's create a grayscale colour
tex = octane.node.create{ type=octane.NT_TEX_FLOAT, name="GrayScale", position={ 400, 400 } }
-- connect the grayscale colour to the bump pin of the material
mat:connectTo(octane.P_BUMP, tex)
-- see if the connection was really made
print(mat:getConnectedNode(octane.P_BUMP) == tex)
-- get the value of the grayscale texture through the pin.
print(mat:getPinValue(octane.P_BUMP)[1])
-- set the value again through the pin, if the value is out of the range of this pin
-- [0, 1] in this case, it will be clamped if set via the pin
mat:setPinValue(octane.P_BUMP, { -1, 0, 0 })
-- get the value directly from the texture, it should be clamped to 0
print(tex:getAttribute(octane.A_VALUE))
-- now disconnect the node again by passing in nil
mat:disconnect(octane.P_BUMP)
print(mat:getConnectedNode(octane.P_BUMP))
Graphs are simply put a container of items. A project always has a single root graph, the scene graph, which can be accessed via octane.node.getRootGraph(). Items are added to a graph on creation. Altough it appears in the standalone that graphs have pins, they don't. Graphs get their input via input linker nodes (prefix NT_IN) and output via output linker nodes (PREFIX NT_OUT). There's nothing special about them, all they do is pass a connection through from a node outside the graph to a node inside (or vica versa). We can get the items in a graph, search for stuff or copy from one graph to another:
- Code: Select all
-- helper to generate a random position so we don't create
-- all the items on top of each other.
function rndPos()
return { math.random(400, 600), math.random(400, 600) }
end
-- create a graph in the scene's root graph
g = octane.nodegraph.create{ type=octane.GT_STANDARD, name="Black Box", position=rndPos() }
-- an output linker
outLink = octane.node.create{ type=octane.NT_OUT_FLOAT, name="Output", graphOwner=g, position=rndPos() }
-- create an input linker
inLink = octane.node.create{ type=octane.NT_IN_FLOAT, name="Input", graphOwner=g, position=rndPos() }
-- let's just connect through the graph (not that usefull ;)
outLink:connectTo(octane.P_INPUT, inLink)
-- now let's add some random nodes in the graph
for i=1,3 do
octane.node.create{ type=octane.NT_BOOL, name="Random Noise", graphOwner=g, position=rndPos() }
end
-- let's find out what is in our graph
print("-- items in the graph")
for i, v in ipairs(g:getOwnedItems()) do
print(i, v)
end
-- lets find all the bool nodes
print("-- bool nodes")
for i,v in ipairs(g:findNodes(octane.NT_BOOL)) do
print(i, v)
end
-- lets copy all the bool nodes directly in the scene graph
octane.nodegraph.getRootGraph():copyFrom(g:findNodes(octane.NT_BOOL))
-- let's create a graph and copy all the cruft in there
copy = octane.nodegraph.create{ type=octane.GT_STANDARD, name="Copy", position=rndPos() }
copy:copyFromGraph(g)
There's a special flavor of graphs called root graphs. We already encoutered one, the scene graph. Root graphs can do everything a plain graph can but they don't have an owner (and you can set an animation time on them, but more on that later). When created, they don't appear in your project and they're automatically destroyed when the script ends. They come in handy as a temporary storage for the scene graph. For example if you don't want your script to modify the original project. Because they don't have an owner, they're not coupled to a project so you could use them to copy between projects:
- Code: Select all
-- load a project
octane.project.load("/home/thomas/Documents/octane-projects/cube-test.ocs")
-- create a root graph (without an owner)
rootGraph = octane.nodegraph.createRootGraph("Copier")
-- copy the whole scene in the root graph
rootGraph:copyFromGraph(octane.nodegraph.getRootGraph())
-- create a new project
octane.project.reset()
-- dump the content from our graph in the new project
octane.nodegraph.getRootGraph():copyFromGraph(rootGraph)
That wraps up the basic node and graphs stuff. I hope the modules octane.node and octane.nodegraph make a bit more sense now. If something is not clear, please ask.
cheers,
Thomas