------------------------------------- -- Wireframe. -- Replaces polygon edges with hairs and vertices with spheres. -- @author Otoy -- @version 1.1 -- @script-id 66156b5a-bc80-4ae3-99f8-6da6bb99691c -- @node-registry-category |Geometry|Wireframe ----------------------------------------- local inputLinkersInfo = {} local outputLinkersInfo = {} local inputLinkers = {} local outputLinkers = {} local nodeGeometry = nil ---------------------------------------------------- -- Returns a key to uniquely identify polygon edges based on the endpoints indices. local function getEdgeKey(p1, p2) if p1 < p2 then return string.format("%d;%d", p1, p2) else return string.format("%d;%d", p2, p1) end end ---------------------------------------------------- -- Process one mesh node: replace edges by hairs and vertices by spheres. local function processMeshNode(script, node, buildGeometry) -- Update the sphere radius and hair thickness local sphereRadiuses = { script:getInputValue(inputLinkers.vertexRadius) } local hairThickness = { script:getInputValue(inputLinkers.edgeRadius) * 2 } node:setAttribute(octane.attributeId.A_SPHERE_RADIUSES, sphereRadiuses, false) node:setAttribute(octane.attributeId.A_HAIR_THICKNESS, hairThickness, false) if not buildGeometry then -- We're done, we've been through the geometry in a previous pass. node:evaluate() return end local verticesPerPoly = node:getAttribute(octane.attributeId.A_VERTICES_PER_POLY) local polyVertexIndices = node:getAttribute(octane.attributeId.A_POLY_VERTEX_INDICES) local vertices = node:getAttribute(octane.attributeId.A_VERTICES) if polyVertexIndices == nil or verticesPerPoly == nil or #verticesPerPoly == 0 or vertices == nil or #vertices == 0 then -- Skip the mesh if it doesn't have any polygons. return end -- Get rid of the filename attribute, -- In the case of an .OBJ based mesh node, this prevents the node evaluation from importing back the OBJ file, -- which would overwrite our attributes. node:setAttribute(octane.attributeId.A_FILENAME, "") node:updateProperties({name=node.name.."_copy"}) -- Deduplicating maps. local mapVertices = {} local mapEdges = {} -- Data arrays we are filling. local sphereCenters = {} local verticesPerHair = {} local hairVertices = {} -- Loop over all polygons. local polyFirstVertexIndex = 1 for i=1, #verticesPerPoly do -- Loop over vertices of the polygon. local count = verticesPerPoly[i] for j=0, count-1 do local ix1 = polyFirstVertexIndex + j local ix2 = polyFirstVertexIndex + ((j + 1) % count) local vIx1 = polyVertexIndices[ix1] + 1 local vIx2 = polyVertexIndices[ix2] + 1 if not mapVertices[vIx1] then mapVertices[vIx1] = true; table.insert(sphereCenters, vertices[vIx1]) end if not mapVertices[vIx2] then mapVertices[vIx2] = true table.insert(sphereCenters, vertices[vIx2]) end local key = getEdgeKey(vIx1, vIx2) if not mapEdges[key] then mapEdges[key] = true table.insert(verticesPerHair, 2) table.insert(hairVertices, vertices[vIx1]) table.insert(hairVertices, vertices[vIx2]) end end polyFirstVertexIndex = polyFirstVertexIndex + count end -- Add hair and sphere data. node:setAttribute(octane.attributeId.A_SPHERE_CENTERS, sphereCenters, false) node:setAttribute(octane.attributeId.A_VERTICES_PER_HAIR, verticesPerHair, false) node:setAttribute(octane.attributeId.A_HAIR_VERTICES, hairVertices, false) -- Remove polygon data. node:setAttribute(octane.attributeId.A_VERTICES_PER_POLY, {}, false) node:setAttribute(octane.attributeId.A_POLY_VERTEX_INDICES, {}, false) node:setAttribute(octane.attributeId.A_VERTICES, {}, false) node:evaluate() end ------------------------------------------------------------------------ -- Recursively process an item. local function processItem(script, node, buildGeometry) if node == nil then return end if node.type == octane.nodeType.NT_GEO_MESH then processMeshNode(script, node, buildGeometry) elseif node.isOutputLinker then -- In this case the downstream node was a nodegraph, a geo archive, a scripted graph or a wrapped scripted graph. -- We are now inside the graph, walk up and continue. processItem(script, node:getConnectedNode(octane.pinId.P_INPUT, true), buildGeometry) else -- In this case the downstream node was some other geometry node. -- For example a Geometry group, a scatter node, a volume, a placement, etc. -- We'll loop over all the inputs and process them. -- There is a potential pitfall here. If we are inside a scripted graph, modifying the input -- will trigger `eval()` on the script we are in and the node we are looping over will be -- destroyed and recreated. This is the case with the Scatter tools for example. for i=1, node:getPinCount() do local pinInfo = node:getPinInfoIx(i) if pinInfo.type == octane.pinType.PT_GEOMETRY then processItem(script, node:getInputNodeIx(i, true), buildGeometry) end end end end -------------------------------------------------- -- Destroy an item and all its inputs recursively. local function destroyItemTree(item) if item == nil then return end if item.isOutputLinker and item.graphOwned then item = item.graphOwner end if item.isGraph then local inputNodes = item:getInputNodes() for _, inputNode in ipairs(inputNodes) do local connectedNode = inputNode:getConnectedNode(octane.pinId.P_INPUT, false) destroyItemTree(connectedNode) end else for pinIx = 1, item:getPinCount() do local inputNode = item:getInputNodeIx(pinIx, false) destroyItemTree(inputNode) end end item:destroy() end -------------------------------------------------------------- -- Destroy the internal copy of the input node and disconnect the output. local function clean() outputLinkers.output:disconnect(octane.pinId.P_INPUT) destroyItemTree(nodeGeometry) nodeGeometry = nil end ----------------------------------------- -- Copy the input node into our own graph. local function copyInput(graph, linker) local inputItem = linker:getConnectedNode(octane.pinId.P_INPUT, false) if inputItem == nil then return nil end if inputItem.isOutputLinker then inputItem = inputItem.graphOwner end local copy = graph:copyItemTree(inputItem) return copy end ------------------------------------ -- The input geometry was changed, re-process it. local function updateInternals(script, graph, init) local buildGeometry = script:inputWasChanged(inputLinkers.inputGeometry) or init if buildGeometry then clean() nodeGeometry = copyInput(graph, inputLinkers.inputGeometry) end if nodeGeometry == nil then return end -- Connect to our output. -- We do this first so we can use our output linker as the start of the recursion. -- Otherwise there is no way to know if the input node is a normal node or a wrapper node. if nodeGeometry.isGraph then local outputNode = nodeGeometry:findFirstOutputNode(octane.pinType.PT_GEOMETRY) if outputNode ~= nil then outputLinkers.output:connectTo(octane.pinId.P_INPUT, outputNode) end else outputLinkers.output:connectTo(octane.pinId.P_INPUT, nodeGeometry) end local item = outputLinkers.output:getConnectedNode(octane.pinId.P_INPUT, true) processItem(script, item, buildGeometry) end ----------------------------------------------------- local function getInputLinkersInfo() local linkerInfoInputGeometry = { key = "inputGeometry", label = "Geometry", description = "Geometry that will be duplicated and replaced with its wireframe version.", type = octane.pinType.PT_GEOMETRY, defaultNodeType = octane.nodeType.NT_GEO_MESH, } local linkerInfoSphereRadius = { key = "vertexRadius", label = "Vertex radius", description = "Radius of spheres replacing the vertices of the original mesh.", type = octane.pinType.PT_FLOAT, defaultNodeType = octane.nodeType.NT_FLOAT, defaultValue = 0.01, bounds = {0.0001, 100}, logarithmic = true } local linkerInfoHairThickness = { key = "edgeRadius", label = "Edge radius", description = "Radius of hairs replacing the edges of the original mesh.", type = octane.pinType.PT_FLOAT, defaultNodeType = octane.nodeType.NT_FLOAT, defaultValue = 0.01, bounds = {0.0001, 100}, logarithmic = true } return {linkerInfoInputGeometry, linkerInfoSphereRadius, linkerInfoHairThickness} end local function getOutputLinkersInfo() local linkerInfoOutput = { key = "output", label = "Geometry out", type = octane.pinType.PT_GEOMETRY } return { linkerInfoOutput } end local function setInputLinkers(graph) local inputs = graph:setInputLinkers(inputLinkersInfo) for i, info in ipairs(inputLinkersInfo) do inputLinkers[info.key] = inputs[i] end end local function setOutputLinkers(graph) local outputs = graph:setOutputLinkers(outputLinkersInfo) for i, info in ipairs(outputLinkersInfo) do outputLinkers[info.key] = outputs[i] end end -------------------------------------------- local node = {} node._name = "Wireframe" node._icon = octane.image.fromBase64("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACF0lEQVQ4jZWSX0hTcRTHP3f3ZqlbLBnoXJhoEc1hy/XHdA+jP64Mi8hQkAoKgtbTiCh6C6KHQIhCe7MSeomIIDAMQqtpD2YJMrOgqczpNJ3byt1NN9eDOSy5G31ffvwO53zOOV+OSHplAQbAAYwAESCZoeYv5YuidL3+3AW/SlrnBLT/UywAu082nhkYC0aSZeY9nYDp3yRVGkAusL2+saHcPTmH5cCxGmAboAbEPw0QFYpFoDQnV33qUfvDvW/6h8kVRT5+cGmWor+0kkBZAsLAvJSm+/725y8vzslLhH0+pnvec/N09dGDdXZ7PL6YbDh79b4nFGlTWqHAtKuirrpynzQ2MY00PoJZF8D54JlQvrNUrDD8lF48vuPcsUnTpDiByWyxxpICfv8Mqlk/thN28HXBvJfZ3kH4PMWhLYWXlADzX4fcPZ8G3ceTkRhZKgFZjpIYcfHj7QC+vnHiagMxObIoKAA2AiXAEZ2+qNagK9gqBDz6d23NuO+2oNJtJhwKcf5V1zUlACxfYTagAcQaU/FopTGbueEE8ag883p04t73hcRTCaCpytjC8rmuqPVJ79BlYAEIAUKeegOHbXpuffHQ+c1bC0wCUyseOGpteVgtWlz9QTq6A46mKuNqIMWF6ynKz0GnyQHoW4mnTLRatKm3ozvA7Ssla3byeOU1sRTA1R9MTQBwo9mj5E3r6o+QwYOM+g29HLcZ+SC1fAAAAABJRU5ErkJggg==", 3) node._evaluateAllChanges = true -- Callback called after initializing the scripted graph with the code. function node.onInit(self, graph) graph:setAttribute(octane.attributeId.A_COLOR, { 1.0, 0.5156, 0.8932 }, true) inputLinkersInfo = getInputLinkersInfo() setInputLinkers(graph) outputLinkersInfo = getOutputLinkersInfo() setOutputLinkers(graph) updateInternals(self, graph, true) end -- Callback called when the connected value of one of the input linker nodes changes. function node.onEvaluate(self, graph) updateInternals(self, graph, false) end return node