Field of View GetCameraAngle Issues

Hello!

I’m currently trying to set up a camera so that it chooses a random position in spherical coordinates around the origin (within specified ranges) and then zooms the camera into the extents of the object. The models should be loaded in with the object centered on the origin, but I correct for this anyways in the script. Later in the script, I need to use the FOV of the camera, but I’ve been having difficulty consistently getting it. Getting Field of View of a ViewPort inside of RhinoCommon recommended I use the function GetCameraAngle(), which is my current way of doing it, but for some reason the function occasionally fails, and even when it succeeds, the angle returned is inconsistent.

Here’s a snippet of the Python code I’m using (with extra comments and slightly modified indentation due to the removal of irrelevant code):

current_view = sc.doc.Views.ActiveView.ActiveViewport

... # Loading Model Code ...

object = Rhino.RhinoDoc.ActiveDoc.Objects.FindByLayer("CustomLayer")[0]
geom = object.BrepGeometry
bb_object = object.BrepGeometry.GetBoundingBox(True)
rs.MoveObject(object, -bb_object.Center)

# Number of times the call to GetCameraAngle() returned False    
fail_iters = 0

# To avoid infinite looping, I have a maximum number of attempts, currently set to 10    
while fail_iters < 10:

    # Choose random spherical coordinates within range
    phi = math.radians(random.randint(30,150))
    theta = math.radians(random.randint(0,359))
    radius = 40
    
    # Set camera location based on coordinates    
    camLoc = Rhino.Geometry.Point3d(radius*math.sin(phi)*math.cos(theta), radius*math.sin(phi)*math.sin(theta), radius*math.cos(phi))
                
    # Zoom into object 
    rs.ViewCameraTarget(camera=camLoc, target=rg.Point3d(0,0,0))
    bb_object = object.BrepGeometry.GetBoundingBox(True)
    rs.ZoomBoundingBox(bb_object)
        
    # Re-aquire new camera position post-zoom
    camLoc, _ = rs.ViewCameraTarget(target=rg.Point3d(0,0,0))
        
    # Half-angle arguments for the GetCameraAngle() method
    d_hangle = clr.StrongBox[float]()
    v_hangle = clr.StrongBox[float]()
    h_hangle = clr.StrongBox[float]()
        
    # If GetCameraAngle() succeeds, then break out of the loop, otherwise continue
    if current_view.GetCameraAngle(d_hangle, v_hangle, h_hangle):
        break
        
    fail_iters += 1
    
print("Number of failures: " + str(fail_iters))
print("Horizontal Half-Angle: " + str(h_hangle.Value))

... # Additional working code for hopefully cool stuff ...

And here’s screencaps of the printouts I got for 4 consecutive runs of the code:

I had initially suspected that the random camera position was what lead to the success/failure of the function call, but this clearly isn’t the case. As we can see from the printouts, it either succeeds on the first try or fails the maximum number of times before leaving the loop. I also confirmed that the exact same position leads to different results by plugging in constants instead of random values for the coordinates (I also load up the same model every time, for the record).

Furthermore, even when the call is successful, the horizontal angle returned is often different, and this is true even when I set the spherical coordinates to constant values. For testing, I set the coordinates to phi=math.radians(60) and theta=math.radians(135), and ran it quite a few times (50+). I got horizontal half angles of: 0.2056, 0.2355, 0.2364, 0.4466, 0.7854, 1.050 and 1.523 on successful calls and always 0 on unsuccessful calls. This is also on the same viewport for the record, it is not the result of me switching between different viewports by accident; I checked to make sure. I find it quite interesting that it only seems to switch between these specific values, as I ran it enough times to confirm that these weren’t randomly selected.

If anyone has any idea what’s going on hear, I’d appreciate the insight, as the documentation for this method is missing a description for the method and when/how it returns a failure. Thanks a bundle!

EDIT: Another quick, interesting observation: while most of those angles are largely devoid of semantic meaning as far as I can tell, the angle 0.7854 is 45 degrees.

I have made some additional discoveries. It seems that the strange behavior is happening because I am loading the model from a file just before calling the GetCameraAngle() method.

I made a new, simpler script to test things, shown below:

import rhinoscriptsyntax as rs
import scriptcontext as sc
import clr

current_view = sc.doc.Views.ActiveView.ActiveViewport

# I have a dataset of models with each model contained in an indexed folder, 
# so here I open the first model; normally I would replace str(0) with str(index)
# and loop through all of the folders
dataset_path = "<PATH_TO_DATASET>" # Loading Line 1
model_dir = dataset_path + "Model_" + str(0) + "\\" # Loading Line 2
rs.Command("_-Open " + model_dir + "Model.3dm", True) # Loading Line 3

d_hangle = clr.StrongBox[float]()
v_hangle = clr.StrongBox[float]()
h_hangle = clr.StrongBox[float]()

if current_view.GetCameraAngle(d_hangle, v_hangle, h_hangle):
    print("Horizontal HalfAngle: " + str(h_hangle.Value))
else:
    print("FAILURE")

This script gives me the same erratic behavior as in the original post. However, if I comment out the three loading lines (labeled ‘Loading Line 1 - 3’ in the snippet above), the method always succeeds and returns 0.2364. It therefore seems as though loading the model and then immediately trying to adjust the camera is leading to the strange behavior. Fortunately, for my case I can simply replace the call to GetCameraAngle() with 0.2364 now that I know what the correct answer is, but I’d still like to know why loading the model is leading to this weird behavior.

This also isn’t the first time I’ve noticed strange interactions between Rhino scripting and loading models either. I was tempted to make a separate post about this, but given the similarity in the root cause for the strange behavior, I think its worth including here (happy to repost again in a separate thread if that’s helpful). Consider the following code snippet, intended to load 10 models and get relatively evenly distributed points on the surface of each by using the vertices from the mesh returned by QuadRemeshBrep():

import rhinoscriptsyntax as rs
import Rhino.Geometry as rg
import scriptcontext as sc
import clr
import Rhino
import time

dataset_path = "<PATH_TO_DATASET>"

old_doc = sc.doc
sc.doc = Rhino.RhinoDoc.ActiveDoc

qr_params = Rhino.Geometry.QuadRemeshParameters()
qr_params.AdaptiveQuadCount=True
qr_params.TargetQuadCount = 200
qr_params.AdaptiveSize = 50
qr_params.DetectHardEdges=True

for h in range(10):
    model_dir = dataset_path + "Model_" + str(h) + "\\"
    rs.Command("_-Open " + model_dir + "Model.3dm", True)
    
    object = Rhino.RhinoDoc.ActiveDoc.Objects.FindByLayer("CustomLayer")[0]
    geom = object.BrepGeometry
    bb_object = object.BrepGeometry.GetBoundingBox(True)
    rs.MoveObject(object, -bb_object.Center)
    
    mesh = rg.Mesh.QuadRemeshBrep(object.BrepGeometry, qr_params)
    verts = mesh.Vertices
    
    # Save verts using csv library

    print(h)
    
print("Complete.")

sc.doc = old_doc

This script mostly works, but how it does so is bizarre. It appears to try to load all of the models and fails for every one, then goes back through and successfully loads all but the first two. Here’s the printout of running the above script.

Somehow, it hits the end condition of the loop twice and skips the first two entries of the loop on the second run through. Also, if I store the output of rs.Command(...) in a variable and print it immediately after, here’s what the printout looks like:

Note that if I ran the script on 20 models, the same behavior occurs, but instead of counting up to 9 it counts to 19. It still misses the first two entries on the second loop through, and the 9 at the beginning of the second loop is replaced with 19.

Interestingly enough, it seems like the script is concerned only with the number of times that the models are loaded in the script rather than the fact that it was done inside a loop, as calling rs.Command(...) twice before the loop to open arbitrary models allowed me to get results for all the models opened in the loop.

Given that in both cases the weird behavior seems to be happening on loading, is there any way to delay executing my post-loading code until after the model has been properly loaded in its entirety?

Hi @John_Wolford,

since i cannot run your code with the same files, i can only speculate. I would try to break down your problems into smaller bits first so others can repeat what you get.

Some thoughts: have you tried to pause your script for a short time before making calculations after a file has been loaded ?

Yes. try adding a Rhino.RhinoApp.Wait() or rs.Sleep(500) before calling
GetCameraAngle.

Is the view you use as current_view always the same for all files, eg. a perspective view after opening each file ? (I do not see where you set the view so it seems you expect that the correct view is active by default after loading a file).

I would suggest a different name for object as it is a reserved keyword in Python.

Try to script only the loading / closing loop using _EditPythonScript without GH. Print the content of the model_dir to the command line just to make sure the paths are correct. Also take care of the return value of rs.Command("_-Open "), it should be True, otherwise stop the loop.

_
c.

I’ve tried running the code from your first post with a simple box on “CustomLayer” in a perspective view (maximized). Running it 20 times, the output is always the same:

Number of failures: 0
Horizontal Half-Angle: 0.350656609951

Do you have a space mouse or similar connected ?

_
c.

Thanks for your response, it was incredibly helpful! I think I’ve at least figured out the GetCameraAngle() problem now thanks to your comments.

My issue is that I initialize current_view to sc.doc.Views.ActiveView.ActiveViewport at the beginning of the loop, but because I opened different models in the loop, the original view needed to be updated to avoid the strange behavior. Adding the line:

current_view = sc.doc.Views.ActiveView.ActiveViewport

just after the rs.Command(...) line fixed the problem for me. I do still get an error saying that sc.doc.Views.ActiveView is NoneType, but the code runs correctly even with this error (normally I would’ve expected it to break after hitting the error, but for some reason it doesn’t).

As for the QuadRemesh problem, I’d already tried the standard time.sleep(), and I’ve now tried rs.Sleep(2000), but neither seems to impact the strange behavior I’ve noticed. I also switched the variable object to my_object to avoid keyword issues (didn’t realize it was a keyword in Python) but that didn’t seem to impact anything. I’ve also replaced the loading line with loading the same model repeatedly, so the strange behavior doesn’t seem to be the result of inter-model relationships. Moreover, if you replace the rs.Open() line with any test model of your choosing, you should still get the strange behavior unless it is specific to my local installation.

Your suggestion to remove Grasshopper and use _EditPythonScript from Rhino DID fix the problem for me, which suggests it has something to do with the interplay between Grasshopper and Rhino, and is not actually a problem on Rhino’s end.

Also to answer your final comment, I have a fairly standard USB mouse, if that’s relevant.

Hi @John_Wolford, you might check if the sc.doc is None, since you switch the doc for GH. If this error does not happen when running the code via EditPythonScript i asume this could be the reason.

_
c.