-- -- This script moves the Camera's position to the current Camera's Target location. -- In this way a user can position a Camera in the Scene by using Octane's -- Target Picker in the render viewport. -- -- Optionally the user can set the eye elevation to a specified height above the -- current Target Position in the current Octane View. This way they can always -- click on a floor or ground plane to spacify the x/y location and the script will -- set the y height consistently above that plane. The user can opt to create a -- copy of the original camera node so that it is not lost when its position coordinates modified. -- If the name of the original camera contains a numeral as its last character, and is optionally -- copied, a new camera is created with the numeral incremented by 1. E.g. 'Working Camera 01' will -- be retained, and a new camera with the new target and position parameters will be created an -- named 'Working Camera 02'. -- -- It can draw a cube at the target location, however the code is commented out as this was -- not so usefull, but I left it in there incase someone else thinks differently. You will need to -- plug it created cube's Geometry Node into a Geometry Group to see it in your scene. -- -- This script is vey helpful in setting up a series of views to be included -- in a VR Tour for use by the Create VR Tour script. For instance it is easy to set up a path for the tour to follow. -- It is well worth using the the short cut 'ctrl T' if making such a path with many views. -- -- If you want to edit the script, there a several print statements that cam be uncommented that will help with debugging. -- -- @author Mark Bassett -- @version 1.0 -- Last revised 6/18/2023 -- @shortcut ctrl + t local version = "1.0" function tprint (t, s) for k, v in pairs(t) do local kfmt = '["' .. tostring(k) ..'"]' if type(k) ~= 'string' then kfmt = '[' .. k .. ']' end local vfmt = '"'.. tostring(v) ..'"' if type(v) == 'table' then tprint(v, (s or '')..kfmt) else if type(v) ~= 'string' then vfmt = tostring(v) end print(" Table entry "..(s or '')..kfmt..' = '..vfmt) end end end -- GUI-------------------------------------------------------------- -- Helper to pop-up an OkCancel dialog and return the button clicked function showWarning(text,msg) local ret = octane.gui.showDialog { type = octane.gui.dialogType.BUTTON_DIALOG, buttonTexts = {"Exit"}, icon = octane.gui.dialogIcon.WARNING, title = "WARNING:\n"..tostring(text), text = tostring(msg), } end -- Create the padding label, used a bottom of form local lblPad = octane.gui.create { type = octane.gui.componentType.LABEL, text = "", width = 420, height = 10, center = true, } local lblRotate = octane.gui.create { type = octane.gui.componentType.LABEL, text = "Rotate Right", width = 420, height = 10, center = true, tooltip = "Rotates the camera about it's current position to the right by the specified amount." } local lblPan = octane.gui.create { type = octane.gui.componentType.LABEL, text = "Pan Right", width = 420, height = 10, center = true, tooltip = "Pans the camera from it's current position to the right by the specified amount." } local lblEyeHeight = octane.gui.create { type = octane.gui.componentType.LABEL, text = "Elevation above Target (eye height) =", width = 185, } -- create a numeric field for the width Plane dimension local txtEyeHeight = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, name = "numEyeHeight", width = 40, enable = true, text = "1.65", } -- create the meter label local lblUnits = octane.gui.create { type = octane.gui.componentType.LABEL, text = "meters", width = 35, } -- used to identify position and target location in the scene by creating a cube. function createCube(name, posX, posY, posZ) x = .1 -- x dimension in current import units z = .1 -- z dimension in current import units xo = posX zo = posZ t = .2 -- thickness in current import units e = posY + t/2 -- elevation of top in current import units vertices = { { -x+xo, e-t, z+zo}, { -x+xo, e, z+zo}, { x+xo, e, z+zo}, { x+xo, e-t, z+zo}, { -x+xo, e-t, -z+zo}, { -x+xo, e, -z+zo}, { x+xo, e, -z+zo}, { x+xo, e-t, -z+zo}, } verticesPerPoly = { 4, -- face1 4, -- face2 4, -- face3 4, -- face4 4, -- face5 4, -- face6 } polyVertexIndices = { 3 , 2 , 1 , 0 , -- face1 - Front 0 , 1 , 5 , 4 , -- face2 - Left 6 , 5 , 1 , 2 , -- face3 - Top 3 , 7 , 6 , 2 , -- face4 - Right 4 , 7 , 3 , 0 , -- face5 - Bottom 4 , 5 , 6 , 7 -- face6 - Back } -- Material names materialNames = { "Cube" } polyMaterialIndices = { 0, 0, 0, 0, 0, 0 } -- create the mesh node, at the specified position in the graph meshNode = octane.node.create{ type = octane.NT_GEO_MESH , name = name, position = { 500,500} } -- set up the geometry attributes. don't evaluate meshNode:setAttribute(octane.A_VERTICES , vertices , false) meshNode:setAttribute(octane.A_VERTICES_PER_POLY , verticesPerPoly , false) meshNode:setAttribute(octane.A_POLY_VERTEX_INDICES , polyVertexIndices , false) meshNode:setAttribute(octane.A_MATERIAL_NAMES , materialNames , false) meshNode:setAttribute(octane.A_POLY_MATERIAL_INDICES , polyMaterialIndices , false) -- evaluate the geometry meshNode:evaluate() return meshNode end -- for layouting all the elements of the Planel dimensions use a group local grpInputs= octane.gui.create { type = octane.gui.componentType.GROUP, text = "Inputs", rows = 1, cols = 3, children = { lblEyeHeight, txtEyeHeight, lblUnits }, padding = { 2 }, inset = { 5 }, border = false, } -- BUTTONS -- local btnMove = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Move to Target", width = 120, height = 20, tooltip = "Move camera position to target location.", } local chkLookBack = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "Look Back", enable = true, width = 170, checked = true, } local chkCopyCamera = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "Copy Camera", enable = true, width = 100, checked = true, } local sldRotate = octane.gui.create { type = octane.gui.componentType.SLIDER, -- type of the component name = "Rotate", -- name of the component value = 0, -- value of the slider minValue = 0, -- minimum value of the slider maxValue = 360, -- maximum value of the slider step = 1, -- interval between 2 discrete slider values width = 420, -- width of the slider in pixels height = 20 , -- height of the slider in pixels enable = true, tooltip = "Angle to rotate about current position in 1 deg. increments.", } local sldPan = octane.gui.create { type = octane.gui.componentType.SLIDER, -- type of the component name = "Pan", -- name of the component value = 0, -- value of the slider minValue = 0, -- minimum value of the slider maxValue = 10, -- maximum value of the slider step = .25, -- interval between 2 discrete slider values width = 420, -- width of the slider in pixels height = 20 , -- height of the slider in pixels enable = true, tooltip = "Distance to Pan from current position in 250mm increments.", } local grpButtons = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 1, cols = 3, children = { chkCopyCamera, chkLookBack, btnMove}, center = true, padding = { 5 }, border = false, } local grpSlider = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 5, cols = 1, children = {lblRotate, sldRotate, lblPan, sldPan, lblPad}, center = true, padding = { 5 }, border = false, } -- group that layouts the other groups local grpLayout = octane.gui.create { type = octane.gui.componentType.GROUP, text = "", rows = 3, cols = 1, children = {grpInputs, grpButtons, grpSlider}, center = true, padding = { 2 }, border = false, debug = false, -- true to show the outlines of the group, handy } -- window that holds all components local wndParent = octane.gui.create { type = octane.gui.componentType.WINDOW, text = "Move Camera's Position to its Target location "..version, children = { grpLayout }, width = grpLayout:getProperties().width, height = grpLayout:getProperties().height, } -- END GUI CODE ----------------------------------------------------------- -- Enable the UI btnMove:updateProperties{enable = true} -- Set variables numEyeHeight = txtEyeHeight.text -- get selected render target local renderTargetNode = nil local tblConnectedNodes = nil local tblUserSelection = octane.project.getSelection() local nodeUserSelection = octane.project.getSelection()[1] local camCopy = nil local flgRendering = nil --if nodeUserSelection ~= nil then --print (nodeUserSelection.type) --end if nodeUserSelection == nil then showWarning("Invalid selection.","Please select a Rendertarget with a Camera attatched, \nor select a Camera attatched to a Render Target.") goto exit elseif nodeUserSelection.type == 56 then -- NT_RENDERTARGET renderTargetNode = nodeUserSelection elseif nodeUserSelection.type == 13 then -- NT_CAM_THINLENS -- Get the rendertarget the camera is attached to tblConnectedNodes = nodeUserSelection:getDestinationNodes(nodeUserSelection) if #tblConnectedNodes >1 then showWarning("Camera is attatched to multiple Render Targets.","Please disconnect additional cameras and retry.") goto exit end if #tblConnectedNodes < 1 then showWarning("The selected Camera is not attatched to a Render Target.","Please attatch the Camera and retry.") goto exit end print(" ") print("i, v in pairs ") for i, v in next, (tblConnectedNodes) do --print("Node: ",tblConnectedNodes[i].node) --print("Pin: ",tblConnectedNodes[i].pin) renderTargetNode = tblConnectedNodes[1].node end else showWarning("Invalid selection.","Please select a Rendertarget with a camera attatched, or a thin lens Camera.") goto exit end --print(renderTargetNode.name) -- get camera attatched to selected rendertarget local nodeSelectedCamera = renderTargetNode:getInputNode(octane.P_CAMERA) --print(" ") --print("Selected Camera Name:") --print(nodeSelectedCamera.name) -- Get up, position and target values local posCameraTarget = nodeSelectedCamera:getPinValue(octane.P_TARGET) local posCameraPosition = nodeSelectedCamera:getPinValue(octane.P_POSITION) local numDistanceToTarget = octane.vec.length(octane.vec.sub(posCameraPosition, posCameraTarget)) + 10 local vecUp = nodeSelectedCamera:getPinValue(octane.P_UP) vecUp = octane.vec.normalized(vecUp) -- Calculate view directions local vecInitialViewDirection = octane.vec.normalized(octane.vec.sub(posCameraTarget, posCameraPosition)) local vecCurrentViewDirection = octane.vec.scale(vecInitialViewDirection , 0) -- Create a cube denoting camera target, didn't find this helpful, others might. --print("Creating Target Cube") --tprint(posCameraTarget) --createCube("Cube @ Target location", posCameraTarget[1], posCameraTarget[2], posCameraTarget[3]) --print("Target Cube created") --print("------------------------------------------") -- Create a cube denoting camera position --print("Creating Poaition Cube") --tprint(posCameraPosition) --createCube( "Cube @ Position location",posCameraPosition[1], posCameraPosition[2], posCameraPosition[3]) --print("Position Cube created") --print("------------------------------------------") -- button functions local function MoveToTargetLocation() if renderTargetNode ~= null then if chkCopyCamera.checked == true then -- Get the position of the selected Camera node in the graph editor local tblSelectedCameraPositions = nodeSelectedCamera.position --print(" ") --print ("Selected camera node's position:") --print (unpack(tblSelectedCameraPositions)) -- Create a new camera identical to the selected camera local tblSelectedCameraItems = octane.node.getProperties(nodeSelectedCamera) --print(" ") --print ("New Camera Properties:") --tprint(tblSelectedCameraItems) camNew = octane.node.create(tblSelectedCameraItems) camNew.position = {tblSelectedCameraPositions[1], tblSelectedCameraPositions[2]} camNew:setPinValue(octane.P_POSITION, nodeSelectedCamera:getPinValue(octane.P_POSITION), true) camNew:setPinValue(octane.P_TARGET, nodeSelectedCamera:getPinValue(octane.P_TARGET), true) camNew:setPinValue(octane.P_FOCAL_LENGTH, nodeSelectedCamera:getPinValue(octane.P_FOCAL_LENGTH), true) camNew:setPinValue(octane.P_FOV, nodeSelectedCamera:getPinValue(octane.P_FOV), true) -- Move the selected camera out from under the new camera nodeSelectedCamera.position = {tblSelectedCameraPositions[1]+30, tblSelectedCameraPositions[2]+35} --print(" ") --print(camNew.name .." Position = ") --tprint(camNew:getPinValue(octane.P_POSITION, renderTargetNode:getConnectedNode(octane.P_CAMERA):getPinValue(octane.P_POSITION))) --print(camNew.name .." Target = ") --tprint(camNew:getPinValue(octane.P_TARGET, renderTargetNode:getConnectedNode(octane.P_CAMERA):getPinValue(octane.P_TARGET))) --camNew.name = nodeSelectedCamera.name..(" - original") -- Character classes: Capitalization of the pattern inverts it, e.g. %W = all non alphanumeric -- . all characters -- %a letters -- %c control characters -- %d digits -- %l lower case letters -- %p punctuation characters -- %s space characters -- %u upper case letters -- %w alphanumeric characters -- %x hexadecimal digits -- %z the character with representation 0 -- Rename new camera, if last character is numeric, increment it local strSuffix = string.sub(nodeSelectedCamera.name, -1) --print("Suffix String =",strSuffix) -- Check that suffix is not alphanumeric if(strSuffix:match("%D")) then --print("Suffix String is not numeric") nodeSelectedCamera.name = (nodeSelectedCamera.name..(" - repositioned")) else print("Suffix String is numeric") local numSuffix = tonumber(strSuffix) nodeSelectedCamera.name = nodeSelectedCamera.name:gsub(strSuffix, numSuffix + 1) --nodeSelectedCamera.name = (nodeSelectedCamera.name..(" - "..tonumber(strSuffix) + 1)) --print(numSuffix) end local tblCameraCopyPositions = camNew.position --print(" ") --print ("Position of selected camera's copy:") --print (unpack(tblCameraCopyPositions)) --The following line disconnects the user selected camera, api call seems broken --camCopy = octane.node.copyFrom(renderTargetNode, octane.P_CAMERA, nodeSelectedCamera, true) --camCopy.name = nodeSelectedCamera.name..(" - copy") end print("Camera Target Location = ", posCameraTarget[1], posCameraTarget[2], posCameraTarget[3]) print("Camera Position Location = ", posCameraPosition[1], posCameraPosition[2], posCameraPosition[3]) print("Camera Up Vector = ", vecUp[1], vecUp[2], vecUp[3]) local vecViewRotationDirection = octane.vec.rotate(vecInitialViewDirection, vecUp, math.pi ) -- Set existing target location to be the new position posCameraPosition = nodeSelectedCamera:getPinValue(octane.P_TARGET) posCameraPosition[2] = posCameraPosition[2] + numEyeHeight nodeSelectedCamera:setPinValue(octane.P_POSITION, posCameraPosition, true) -- set new target location if chkLookBack.checked == true then -- look back to original position posCameraTarget = octane.vec.add(posCameraPosition , octane.vec.scale(vecViewRotationDirection, numDistanceToTarget )) else -- look directly ahead posCameraTarget = octane.vec.add(posCameraPosition , octane.vec.scale(vecInitialViewDirection, numDistanceToTarget )) end -- make the target eye elevation the same as the position eye elevation posCameraTarget[2] = posCameraPosition[2] nodeSelectedCamera:setPinValue(octane.P_TARGET, posCameraTarget, true) -- Triggers the view to be updated wndParent:closeWindow() elseif renderTargetNode == null then showWarning("Missing Rendertarget.","Please select a Rendertarget with camera attatched.") wndParent:closeWindow() end end -- callback handling the GUI elements local function guiCallback(component, event) if flgRendering == true then showWarning("Busy Rendering View.","Please wait another 0.5 seconds for rendering to finish before modifying the Camera's attributes.") --goto exit end if component == btnMove then MoveToTargetLocation() elseif component == sldRotate then print("@ Rotating") local vecViewRotateDirection = octane.vec.rotate(vecInitialViewDirection, vecUp, math.rad(-1 * sldRotate.value) ) -- apply new direction vector to camera posCameraTarget = octane.vec.add(posCameraPosition , octane.vec.scale(vecViewRotateDirection, numDistanceToTarget )) nodeSelectedCamera:setPinValue(octane.P_TARGET, posCameraTarget, true) flgRendering = true octane.render.start { renderTargetNode = renderTargetNode , maxRenderTime = 0.5, } flgRendering = false elseif component == sldPan then print("@ Panning") -- Calculate amount to pan local vecInitialCameraUp = nodeSelectedCamera:getPinValue(octane.P_UP) --vecInitialViewDirection = octane.vec.normalized(octane.vec.sub(posCameraTarget, posCameraPosition)) local vecCurrentCameraRight = octane.vec.cross(octane.vec.normalized(vecInitialViewDirection), vecInitialCameraUp) local vecPan = octane.vec.scale(vecCurrentCameraRight, sldPan.value) print("camera position") tprint(posCameraPosition) print("camera target") tprint(posCameraTarget) print("initial camera up Vector") tprint(vecInitialCameraUp) print("initial camera view direction Vector") tprint(vecInitialViewDirection) print("current camera right Vector") tprint(vecCurrentCameraRight) print("Slider Value",sldPan.value) print("Panning Vector") tprint(vecPan) -- Move current camera target & position by pan vector amount posCameraTarget = octane.vec.add(posCameraTarget , vecPan ) print("New Target",sldPan.value) posCameraPosition = octane.vec.add(posCameraPosition , vecPan ) print("New Position",sldPan.value) -- Apply panned direction vector to camera nodeSelectedCamera:setPinValue(octane.P_TARGET, posCameraTarget, true) nodeSelectedCamera:setPinValue(octane.P_POSITION, posCameraPosition, true) print("Camera Pan - Target Location = ", posCameraTarget[1], posCameraTarget[2], posCameraTarget[3]) print (renderTargetNode.name) flgRendering = true octane.render.start { renderTargetNode = renderTargetNode , maxRenderTime = 0.5, } flgRendering = false elseif component == txtEyeHeight then numEyeHeight = txtEyeHeight.text end end -- hookup the callback with all the GUI elements btnMove:updateProperties { callback = guiCallback } txtEyeHeight:updateProperties { callback = guiCallback } sldRotate:updateProperties { callback = guiCallback } sldPan:updateProperties { callback = guiCallback } wndParent:updateProperties { callback = guiCallback } -- Test if a rendertarget is selected local renderTargetNode = octane.project.getSelection()[1] if renderTargetNode ~= null then rv = octane.gui.showWindow(wndParent) elseif renderTargetNode == null then showWarning("No selected Rendertarget.","Please select a Rendertarget with camera attatched.") goto exit end ::exit::