Friday 14 November 2014

FreeCAD: Tour camera

Imagine that you draw a house or a building in FreeCAD with all its details and interior elements. Wouldn't it be awesome if you could get into just like if you were walking inside?
This "tour camera" macro makes it possible.



This is the small "town" I've created for testing this macro. Download it here, get the full macro
code at the bottom of the post, and follow this microtutorial



(notice the camera track in orange)

How does it work?

Very simple, create a sketch over the surface you want to travel and exit. Then, switch view mode to "perspective view", and finally,  at the previous sketch, select one line and paste this at the python console:

from pivy import coin
import time
from FreeCAD import Base
cam = FreeCADGui.ActiveDocument.ActiveView.getCameraNode()
trajectory = Gui.Selection.getSelectionEx()[0].Object.Shape.Edges
for edge in trajectory:
  startPoint = edge.valueAt( 0.0 )
  endPoint = edge.valueAt( edge.Length )
  dirVector = ( endPoint - startPoint ).normalize()
  currentPoint = startPoint
  while (currentPoint - startPoint).Length < edge.Length:
    currentPoint = currentPoint + dirVector
    cam.position.setValue(currentPoint + Base.Vector( 0,0, 10) )
    cam.pointAt( coin.SbVec3f( endPoint[0], endPoint[1], endPoint[2]+10) , coin.SbVec3f( 0, 0, 1 ) )
    Gui.updateGui()
    time.sleep(0.005)

What it does is:

-Gets camera node and sketch track as a list of edges
-Iterates over the list of edges, positioning the camera from edge start to the edge length position, looking at the edge endpoint.

If you did the above, you should be travelling like this:



But is not only your screen the one that shakes badly when you go from one line to the next, mine too.
When camera jumps from one line to the next, it is orientated to the new endpoint in one step. That's the origin of the shaking behavior.

So, with a bit more of complication, I've improved this (new "town" included :):


In this version, camera walks along the actual edge pointing its view to a vector, with a given length and the same direction than the movement. Once this vector reaches the intersection with the next line, the camera rotates to align with the end point of that line. 
In conclusion, you get a smooth transition and a nice walk around.

But that's the behavior roughly speaking, for the real thing, please look the code below.

# JMG November 2014
from pivy import coin
import time
from FreeCAD import Base
camera = FreeCADGui.ActiveDocument.ActiveView.getCameraNode()
trajectory = Gui.Selection.getSelectionEx()[0].Object.Shape.Edges
camHeight = 10   # Height of the camera above the track
lookVectorLength = 80   # Distance from next line start where the camera starts to align with new direction
for i in range( len( trajectory ) - 1):
  currEdge = trajectory[i]
  currEdgeDir = ( currEdge.valueAt( currEdge.Length ) - currEdge.valueAt( 0.0 ) ).normalize()
  nextEdge = trajectory[i+1]
  nextEdgeDir = ( nextEdge.valueAt( nextEdge.Length ) - nextEdge.valueAt( 0.0 ) ).normalize()
  currPos = currEdge.valueAt( 0.0 )
  while (currPos - currEdge.valueAt( 0.0 ) ).Length < currEdge.Length:
    currPos = currPos + currEdgeDir
    camera.position.setValue( currPos  + Base.Vector( 0, 0, camHeight ) )
    cameraLookVector = currEdgeDir*lookVectorLength
    if (cameraLookVector + currPos - currEdge.valueAt(0.0) ).Length > currEdge.Length:
      L = ( cameraLookVector + ( currPos - currEdge.valueAt( 0.0 ) ) ).Length - currEdge.Length
      lookVector = nextEdgeDir*L + nextEdge.valueAt( 0.0 )
      
    else:
      lookVector = currEdge.valueAt( currEdge.Length )
    
    camera.pointAt( coin.SbVec3f( lookVector[0], lookVector[1], lookVector[2] + camHeight ), coin.SbVec3f( 0, 0, 1 ) )
    Gui.updateGui()
    time.sleep( 0.004 )



It features two configurable parameters:

- camHeight: height of the camera above the track ( over z axis )

-lookVectorLength: distance from the endpoint of the current line at witch the camera starts to rotate to align with the next line. Try values, the more it approaches to 0, the more shake you get. But too big values will shake too!

Before leaving, a funny thing: instead of the sketch track, select the 3d model and paste the code.

Code update:

A complex track may involve booleans, and , as every time you operate with a shape their subelements get reorganized, the result is something like what .rpv got trying to tour inside a 3d printer. Curious as a background video in presentations, but not the expected behavior.

The solution is to rearrange the list of edges so next edge start begins at the current edge end. It is achieved this way:

SelectedEdge = Gui.Selection.getSelectionEx()[0].SubObjects[0]
RawTrajectory = Gui.Selection.getSelectionEx()[0].Object.Shape.Edges

# Edge rearrangement inside trajectory list
trajectory = []
trajectory.append( SelectedEdge )
currentEdge = SelectedEdge
for n in range( len( RawTrajectory ) ):
  for edge in RawTrajectory:
    if edge.valueAt(0.0) == currentEdge.valueAt( currentEdge.Length ):
      trajectory.append( edge )
      currentEdge = edge
      break


Full code:

Complete version featuring everything:

# JMG November 2014
from pivy import coin
import time
from FreeCAD import Base
camera = FreeCADGui.ActiveDocument.ActiveView.getCameraNode()
SelectedEdge = Gui.Selection.getSelectionEx()[0].SubObjects[0]
RawTrajectory = Gui.Selection.getSelectionEx()[0].Object.Shape.Edges

# Edge rearrangement inside trajectory list
trajectory = []
trajectory.append( SelectedEdge )
currentEdge = SelectedEdge
for n in range( len( RawTrajectory ) ):
  for edge in RawTrajectory:
    if edge.valueAt(0.0) == currentEdge.valueAt( currentEdge.Length ):
      trajectory.append( edge )
      currentEdge = edge
      break

camHeight = 10   # Height of the camera above the track
lookVectorLength = 80   # Distance from next line start where the camera starts to align with new direction
for i in range( len( trajectory ) - 1):
  currEdge = trajectory[i]
  currEdgeDir = ( currEdge.valueAt( currEdge.Length ) - currEdge.valueAt( 0.0 ) ).normalize()
  nextEdge = trajectory[i+1]
  nextEdgeDir = ( nextEdge.valueAt( nextEdge.Length ) - nextEdge.valueAt( 0.0 ) ).normalize()
  currPos = currEdge.valueAt( 0.0 )
  while (currPos - currEdge.valueAt( 0.0 ) ).Length < currEdge.Length:
    currPos = currPos + currEdgeDir
    camera.position.setValue( currPos  + Base.Vector( 0, 0, camHeight ) )
    cameraLookVector = currEdgeDir*lookVectorLength
    if (cameraLookVector + currPos - currEdge.valueAt(0.0) ).Length > currEdge.Length:
      L = ( cameraLookVector + ( currPos - currEdge.valueAt( 0.0 ) ) ).Length - currEdge.Length
      lookVector = nextEdgeDir*L + nextEdge.valueAt( 0.0 )
      
    else:
      lookVector = currEdge.valueAt( currEdge.Length )
    
    camera.pointAt( coin.SbVec3f( lookVector[0], lookVector[1], lookVector[2] + camHeight ), coin.SbVec3f( 0, 0, 1 ) )
    Gui.updateGui()
    time.sleep( 0.004 )



Enjoy!