1
$\begingroup$

I'm attempting to write a script that will place an object on a "terrain" plane, within the FOV of an existing camera which will be somewhere above that plane, pointing downwards, but with the exact coordinates and view angle randomised each time the code runs. I have the following code, which should find the 4 coordinates on the terrain corresponding to the 4 corners of the camera FOV (by sending raycasts out along the 4 corners of the camera view and getting the locations where they hit the terrain), however it seems to get the 4 corners of the 3D viewport, not the camera (tested by placing primitive cubes at the 4 corner locations. Therefore, when the object is placed, it is not placed within the camera FOV, but within the bounds of the 3D viewport. (For further context, this script will be run in headless mode, so any solution has to be 100% using bpy, not the user interface).

This is the code finding the 4 coordinates of the camera frame on the terrain plane:

def camera_normalized_frame(
    cam: object = None,
) -> List[Vector]:

    render = bpy.context.scene.render
    aspect = (render.resolution_x * render.pixel_aspect_x) / (
        render.resolution_y * render.pixel_aspect_y
    )
    view_frame = cam.data.view_frame(scene=None)
    frame = [-f / view_frame[0].z for f in view_frame]
    if aspect > 1:
        mat = Matrix.Diagonal(
            (1, 1 / aspect, 1),
        )
    else:
        mat = Matrix.Diagonal(
            (aspect, 1, 1),
        )
    for i in range(len(frame)):
        frame[i] = mat @ frame[i]
    return frame

def place_visible_xy(
    target: object = None,
    terrain: object = None,
    camera: object = None,
    ):

    # Get camera frame to constrain raycast direction
    frame = camera_normalized_frame(camera)
    for v in frame:
        hit, loc, _, _, _, _ = bpy.context.scene.ray_cast(
            bpy.context.view_layer.depsgraph,
            origin=camera.location,
            direction=v,
            distance=20000,
        )
        # Add primitives to test hit locations
        bpy.ops.mesh.primitive_cube_add(size=100, location=loc)
$\endgroup$

1 Answer 1

1
$\begingroup$

Try this revised version of your script, have a look at the comments:

def camera_normalized_frame(
    cam: object = None,
) -> List[Vector]:

    render = bpy.context.scene.render
    aspect = (render.resolution_x * render.pixel_aspect_x) / (
        render.resolution_y * render.pixel_aspect_y
    )
    view_frame = list(cam.data.view_frame(scene=None))
 
    # 1. not sure why you need this. Without this line,
    # the test spheres (see below) will be placed
    # correctly at the frustum corners
    #frame = [-f / view_frame[0].z for f in view_frame]
    aspect_matrix = Matrix.Diagonal(
        (1, 1 / aspect, 1) if aspect > 1 else (aspect, 1, 1)  # just made it terser
    )
    for i in range(len(view_frame)):
        view_frame[i] = aspect_matrix @ view_frame[i]

        # 2. see docs: https://docs.blender.org/api/current/bpy.types.Camera.html#bpy.types.Camera.view_frame
        # "Return 4 points for the cameras frame -->(before object transformation)<--"
        # thus we transform the frame positions ourself
        view_frame[i] = cam.matrix_world @ view_frame[i]

    return view_frame

def place_visible_xy(
    target: object = None,
    terrain: object = None,
    camera: object = None,
):

    # Get camera frame to constrain raycast direction
    frame = camera_normalized_frame(camera)
    for v in frame:
        # 3. test spheres to check the frame world positions
        #bpy.ops.mesh.primitive_uv_sphere_add(radius=0.1, location=v)
        hit, loc, _, _, _, _ = bpy.context.scene.ray_cast(
            bpy.context.view_layer.depsgraph,
            origin=camera.location,
            # 4. as the frame positions are in world space,
            # we reconstruct the directions
            direction=v - camera.location, 
            distance=20000,
        )
        # Add primitives to test hit locations
        bpy.ops.mesh.primitive_cube_add(size=1, location=loc)
  

Running the script with the test spheres (enabling # 3. and disabling adding cubes) should give this:

enter image description here

and the final version this:

enter image description here

$\endgroup$
2
  • $\begingroup$ This is a great answer! Thank you for your time in explaining and illustrating it so clearly. $\endgroup$
    – oscr104
    Commented Sep 5, 2023 at 10:18
  • $\begingroup$ Welcome, happy blending! $\endgroup$
    – taiyo
    Commented Sep 5, 2023 at 10:31

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .