-------------------------------------------- -- Bounding box extraction. -- Produces the bounding box of the input geometry tree. -- -- @author OTOY -- @version 0.1 -------------------------------------------- local inputLinkers = {} local outputLinkers = {} local nodeMinValue = nil local nodeMaxValue = nil local bbox = {} ----------------------------------------------------- -- Utilities ----------------------------------------------------- local function vecMin(a, b) return { math.min(a[1], b[1]), math.min(a[2], b[2]), math.min(a[3], b[3]), } end local function vecMax(a, b) return { math.max(a[1], b[1]), math.max(a[2], b[2]), math.max(a[3], b[3]), } end local function vecPrint(p) print("p:"..tostring(p[1])..", "..tostring(p[2])..", "..tostring(p[3])) end -- Initialize the AABB to invalid values ready for insertion by comparison. local function aabbCreate() return { min = {math.huge, math.huge, math.huge}, max = {-math.huge, -math.huge, -math.huge} } end local function aabbIsValid(aabb) local diff = octane.vec.sub(aabb.max, aabb.min) return diff[1] < 0 or diff[2] < 0 or diff[3] < 0 end -- Consolidate a point into the AABB. local function aabbInsert(aabb, p) aabb.min = vecMin(aabb.min, p) aabb.max = vecMax(aabb.max, p) end -- Consolidate aabb2 into aabb1. local function aabbMerge(aabb1, aabb2) aabb1.min = vecMin(aabb1.min, aabb2.min) aabb1.max = vecMax(aabb1.max, aabb2.max) end local function aabbPrint(aabb) print("AABB:") print("\tmin:{"..tostring(aabb.min[1])..", "..tostring(aabb.min[2])..", "..tostring(aabb.min[3]).."}") print("\tmax:{"..tostring(aabb.max[1])..", "..tostring(aabb.max[2])..", "..tostring(aabb.max[3]).."}") end -- --------------------------- -- Recursive function that walks the geometry input tree, collects transforms, -- and updates the global bounding box. local function walkTreeGeometry(nodeGeometry, placementTransform) if nodeGeometry == nil then return end -- TODO: -- Support particles and hairs. -- Support SDF. -- Support volumes. local nodeGeometryType = nodeGeometry:getProperties().type if nodeGeometryType == octane.nodeType.NT_GEO_MESH then -- Termination of this branch. -- Compute the local bounding box and merge it into the global one. local verticesPerPoly = nodeGeometry:getAttribute(octane.attributeId.A_VERTICES_PER_POLY) local polyVertexIndices = nodeGeometry:getAttribute(octane.attributeId.A_POLY_VERTEX_INDICES) local vertices = nodeGeometry:getAttribute(octane.attributeId.A_VERTICES) -- Skip the mesh if it doesn't have any polygons. if polyVertexIndices == nil or verticesPerPoly == nil or #verticesPerPoly == 0 or vertices == nil or #vertices == 0 then return end -- Loop over all polygons. local aabb = aabbCreate() 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 vertexIndex = polyVertexIndices[polyFirstVertexIndex + j] + 1 local p = vertices[vertexIndex] p = octane.matrix.mulP(placementTransform, p) aabbInsert(aabb, p) end polyFirstVertexIndex = polyFirstVertexIndex + count end -- Consolidate this aabb into the global one. aabbMerge(bbox, aabb) elseif nodeGeometryType == octane.nodeType.NT_IN_GEOMETRY then -- We are exiting a nodegraph, continue up. local nodeGeometryInput = nodeGeometry:getConnectedNode(octane.pinId.P_INPUT, true) walkTreeGeometry(nodeGeometryInput, placementTransform) elseif nodeGeometryType == octane.nodeType.NT_OUT_GEOMETRY then -- We are entering a nodegraph, continue up. local nodeGeometryInput = nodeGeometry:getConnectedNode(octane.pinId.P_INPUT, true) walkTreeGeometry(nodeGeometryInput, placementTransform) elseif nodeGeometryType == octane.nodeType.NT_OBJECTLAYER_MAP then -- Object layer map, continue up. local nodeGeometryInput = nodeGeometry:getConnectedNode(octane.pinId.P_GEOMETRY, true) walkTreeGeometry(nodeGeometryInput, placementTransform) elseif nodeGeometryType == octane.nodeType.NT_GEO_GROUP then -- Geometry group, follow each branch. local pinCount = nodeGeometry:getAttribute(octane.attributeId.A_PIN_COUNT) for i=1, pinCount do local nodeGeometryInput = nodeGeometry:getInputNodeIx(i, true) walkTreeGeometry(nodeGeometryInput, placementTransform) end elseif nodeGeometryType == octane.nodeType.NT_GEO_PLACEMENT then -- Apply the transform and continue up. local transform = nodeGeometry:getPinValue(octane.pinId.P_TRANSFORM) placementTransform = octane.matrix.mul(placementTransform, transform) local nodeGeometryInput = nodeGeometry:getConnectedNode(octane.pinId.P_GEOMETRY, true) walkTreeGeometry(nodeGeometryInput, placementTransform) else -- Any other node type stops the recursion. end -- At this point we have explored a branch of the input tree and possibly consolidated the global bounding box. end ---------------------------------------------------------- -- Get the bounding box and updates the internal value nodes. local function updateBoundingBox(script, graph) local nodeGeometry = inputLinkers.geometry:getInputNode(octane.pinId.P_INPUT, true) if nodeGeometry == nil then print("Error: could not find the geometry.") return end bbox = aabbCreate() local placementTransform = octane.matrix.getIdentity() walkTreeGeometry(nodeGeometry, placementTransform) nodeMinValue:setAttribute(octane.attributeId.A_VALUE, bbox.min, true) nodeMaxValue:setAttribute(octane.attributeId.A_VALUE, bbox.max, true) end ------------------------------------------- local function prepare(graph) local function createNode(type, name, owner) return octane.node.create { type = type, name = name, graphOwner = owner } end nodeMinValue = createNode(octane.nodeType.NT_FLOAT, "Min value", graph) nodeMaxValue = createNode(octane.nodeType.NT_FLOAT, "Max value", graph) outputLinkers.outputMin:connectTo(octane.pinId.P_INPUT, nodeMinValue) outputLinkers.outputMax:connectTo(octane.pinId.P_INPUT, nodeMaxValue) octane.nodegraph.unfold(graph, true) end ------------------------------------------- local function setInputLinkers(graph) local liGeometry = { key = "geometry", label = "Geometry", type = octane.pinType.PT_GEOMETRY, defaultNodeType = octane.nodeType.NT_GEO_OBJECT, } local liIsLive = { key = "isLive", label = "Live update", description = "If true the bounding box will be computed on every change in the input.".. "\nIf false it will only be computed when clicking the trigger button.", type = octane.pinType.PT_BOOL, defaultNodeType = octane.nodeType.NT_BOOL, defaultValue = true, } ------------------------------ local linkersInfo = { liGeometry, liIsLive } local inputs = graph:setInputLinkers(linkersInfo) for i, info in ipairs(linkersInfo) do inputLinkers[info.key] = inputs[i] end end ------------------------------------------------------------- local function setOutputLinkers(graph) local liOutputMin = { key = "outputMin", label = "Min value", type = octane.pinType.PT_FLOAT } local liOutputMax = { key = "outputMax", label = "Max value", type = octane.pinType.PT_FLOAT } local linkersInfo = { liOutputMin, liOutputMax } local outputs = graph:setOutputLinkers(linkersInfo) for i, info in ipairs(linkersInfo) do outputLinkers[info.key] = outputs[i] end end -------------------------------------------- local node = {} node._name = "Geometry bounding box" -- Callback called after initializing the scripted graph with the code. function node.onInit(self, graph) graph:setAttribute(octane.attributeId.A_COLOR, { 0.20, 0.35, 0.55 }, true) setInputLinkers(graph) setOutputLinkers(graph) prepare(graph) updateBoundingBox(self, graph) end -- Callback called when the connected value of one of the input linker nodes changes. function node.onEvaluate(self, graph) local isLive = self:getInputValue(inputLinkers.isLive) if self:inputWasChanged(inputLinkers.geometry) and isLive then updateBoundingBox(self, graph) end end function node.onTrigger(self, graph) updateBoundingBox(self, graph) end return node