-- @description Script that copies out the position and orientation of an animated camera of a scene node graph into a new node graph and then sets up some bubble geometry that is placed by this data. Can be used to set up animated cameras in a medium. -- @author me :) -- @shortcut ctrl + b -- @version 0.1 ---------------------------------------------------------------------------------------------------- -- HELPERS -- helper to pop-up an error dialog local function showError(text) octane.gui.showDialog { type = octane.gui.dialogType.BUTTON_DIALOG, title = "Error", text = text, } end ---------------------------------------------------------------------------------------------------- -- UI local function askWhichCamera(cameras) -- determine camera names local camNames = {} for _,node in pairs(cameras) do table.insert(camNames, node.name) end -- create UI components local label = octane.gui.create { type = octane.gui.componentType.LABEL, text = "Please select the camera to copy and set up:", width = 250 } local list = octane.gui.create { type = octane.gui.componentType.COMBO_BOX, items = camNames, selectedIx = 1, width = 150 } local listGroup = octane.gui.create { type = octane.gui.componentType.GROUP, rows = 1, cols = 2, children = { label, list }, padding = { 5 }, border = false } local okButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Ok", width = 50 } local cancelButton = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Cancel", width = 50 } local buttonGroup = octane.gui.create { type = octane.gui.componentType.GROUP, rows = 1, cols = 2, children = { okButton, cancelButton }, padding = { 5 }, border = false } local layoutGroup = octane.gui.create { type = octane.gui.componentType.GROUP, rows = 2, cols = 1, children = { listGroup, buttonGroup }, padding = { 5 }, border = false, centre = true } local dialog = octane.gui.create { type = octane.gui.componentType.WINDOW, text = "", children = { layoutGroup }, width = layoutGroup.width, height = layoutGroup.height } -- set up button callbacks okButton.callback = function() dialog:closeWindow(list.selectedIx) end cancelButton.callback = function() dialog:closeWindow(0) end -- show the dialog and read the selection local selection = dialog:showWindow() -- return nothing if cancelled if not selection or (selection == 0) then return nil -- return the selected camera else return cameras[selection] end end ---------------------------------------------------------------------------------------------------- -- MAIN -- get the selected scene node graph local selectedScene = octane.project.getSelection()[1] if not selectedScene or selectedScene.type ~= octane.GT_GEOMETRYARCHIVE then showError("No scene node graph selected.") return end -- currently, the selected scene node graph needs to be owned by a node graph so we can place -- the camera bubble node graph along side with it if not selectedScene.graphOwned then showError("The selected scene node graph needs to be owned by a node graph and not be internal to a pin.") return end -- determine camera outputs of scene local cameras = {} for _,node in pairs(selectedScene:getOutputNodes()) do if node.outputType == octane.PT_CAMERA then local cameraNode = node:getInputNodeIx(1) if cameraNode and cameraNode.type == octane.NT_CAM_THINLENS then table.insert(cameras, cameraNode) end end end -- bail out if there are no camera nodes if #cameras == 0 then showError("Selected scene node graph doesn't contain any thin-lens camera.") return end -- determine which camera to bubble wrap local selectedCamera = cameras[1] if #cameras > 1 then selectedCamera = askWhichCamera(cameras) if not selectedCamera then return end end -- create a new root node graph and copy camera into it local tempRoot = octane.nodegraph.createRootGraph() local copiedCamera = tempRoot:copyFrom({ selectedCamera })[1] -- determine frame rate, range and number of frames we need to calculate local fps = octane.project.getProjectSettings():getAttribute(octane.A_FRAMES_PER_SECOND) if fps <= 0 then fps = 24 end local range = tempRoot:getAnimationTimeSpan() if range[2] < range[1] then range[2] = range[1] end local frameCount = math.ceil((range[2] - range[1]) * fps) local shutterTime = math.max(0, octane.render.getShutterTime()) -- animate the camera through the whole time range using the project FPS as sample rate. -- for each frame, calculate a transformation matrix that positions and scales a unit cube -- according to the amount of movement of the camera. -- the rotation of the cube (/bubble) will be aligned with the movement of the camera, -- and its viewing direction. local animatedTransforms = {} for i = 0, frameCount, 1 do -- get camera parameters at the start of the shutter time tempRoot:updateTime(range[1] + i/fps, false) local pos = copiedCamera:getPinValue(octane.P_POSITION) local targ = copiedCamera:getPinValue(octane.P_TARGET) local up = copiedCamera:getPinValue(octane.P_UP) local aperture = copiedCamera:getPinValue(octane.P_APERTURE) * 0.01 -- get camera parameters at the end of the shutter time tempRoot:updateTime(range[1] + i/fps + shutterTime, false) local pos2 = copiedCamera:getPinValue(octane.P_POSITION) local targ2 = copiedCamera:getPinValue(octane.P_TARGET) local up2 = copiedCamera:getPinValue(octane.P_UP) aperture = math.max(aperture, copiedCamera:getPinValue(octane.P_APERTURE) * 0.01) -- calculate the pos/target/up vectors at 50% of the shutter time local posM = octane.vec.scale(octane.vec.add(pos, pos2), 0.5) local targM = octane.vec.scale(octane.vec.add(targ, targ2), 0.5) local upM = octane.vec.scale(octane.vec.add(up, up2), 0.5) -- build transformation matrix from the middle vectors local mx, my, mz mz = octane.vec.normalized(octane.vec.sub(targM, posM)) mx = octane.vec.sub(pos, posM) if octane.vec.length(mx) == 0 then -- handle case when there is no movement at all (align with up vector) mx = octane.vec.normalized(octane.vec.cross(upM, mz)) my = octane.vec.normalized(octane.vec.cross(mz, mx)) mx = octane.vec.scale(mx, 0.05) my = octane.vec.scale(my, 0.05) mz = octane.vec.scale(mz, 0.05) else -- handle case when there is movement (align with movement) my = octane.vec.normalized(octane.vec.cross(mz, mx)) mz = octane.vec.normalized(octane.vec.cross(mx, my)) mx = octane.vec.scale(mx, 1.2) my = octane.vec.scale(my, octane.vec.length(mx)) mz = octane.vec.scale(mz, 0.05) end animatedTransforms[i+1] = { { mx[1], my[1], mz[1], posM[1] }, { mx[2], my[2], mz[2], posM[2] }, { mx[3], my[3], mz[3], posM[3] } } end -- delete the temporary graph tempRoot:destroy() -- create new node graph in the owner of the selected scene node graph and add these nodes: -- +------------O------------------+ -- | ^ | -- | +--------------+ | -- | |MediumMatInput| | -- | +------O-------+ | -- | | | -- | +----O-----+ | -- | |BubbleMesh| | -- | +----O-----+ | -- | | | -- | +----O-----@----+ | -- | |BubblePlacement| | -- | +-------O-------+ | -- | | | -- | +-----O------+ | -- | |BubbleOutput| | -- | +------------+ | -- | v | -- +---------------O---------------+ local bubbleGraph = octane.nodegraph.create { type = octane.GT_STANDARD, name = selectedCamera.name .. " bubble", graphOwner = selectedScene.graphOwner, position = { selectedScene.position[1] + 200, selectedScene.position[2] } } local mediumMatInput = octane.node.create { type = octane.NT_IN_MATERIAL, name = "Medium material", graphOwner = bubbleGraph, } local bubbleMesh = octane.node.create { type = octane.NT_GEO_MESH, name = "Bubble", graphOwner = bubbleGraph, } local bubblePlacement = octane.node.create { type = octane.NT_GEO_PLACEMENT, name = "Bubble Placement", graphOwner = bubbleGraph, } local bubbleTransform = octane.node.create { type = octane.NT_TRANSFORM_VALUE, pinOwnerNode = bubblePlacement, pinOwnerId = octane.P_TRANSFORM } local bubbleOutput = octane.node.create { type = octane.NT_OUT_GEOMETRY, name = "Bubble Output", graphOwner = bubbleGraph, } -- set up mesh node bubbleMesh:setAttribute(octane.A_VERTICES, { { 1, -1, 1 }, { -1, -1, 1 }, { -1, 1, 1 }, { 1, 1, 1 }, { 1, -1, -1 }, { -1, -1, -1 }, { -1, 1, -1 }, { 1, 1, -1 } }, false) bubbleMesh:setAttribute(octane.A_VERTICES_PER_POLY, { 4, 4, 4, 4, 4, 4 }, false) bubbleMesh:setAttribute(octane.A_POLY_VERTEX_INDICES, { 0, 1, 2, 3, 1, 5, 6, 2, 5, 4, 7, 6, 4, 0, 3, 7, 3, 2, 6, 7, 1, 0, 4, 5 }, false) bubbleMesh:setAttribute(octane.A_MATERIAL_NAMES, { "Medium" }, false) bubbleMesh:setAttribute(octane.A_POLY_MATERIAL_INDICES, { 0, 0, 0, 0, 0, 0 }, false) bubbleMesh:evaluate() -- set up placement node if #animatedTransforms > 1 then bubbleTransform:setAnimator(octane.A_TRANSFORM, {0}, animatedTransforms, 1/fps) else bubbleTransform:setAttribute(octane.A_TRANSFORM, animatedTransforms[1]) end -- connect the rest bubbleOutput:connectToIx(1, bubblePlacement) bubblePlacement:connectTo(octane.P_GEOMETRY, bubbleMesh) bubbleMesh:connectToIx(1, mediumMatInput) -- unfold the node graph bubbleGraph:unfold() -- DONE!