# Check if the whole plane is being on a orthographic camera render (or get a proportion of the rendered plane)

Situation: On my scene, planes can be positioned anywhere and I should not move them - I move around only camera. After positioning a orthographic camera with a script, I am not sure if the whole plane is being rendered - I want to render one whole plane at a time (also areas that don't belong to a plane are not desired on a render).

My questions:

1. Given a camera position and the plane in 3D space (where camera is orthogonally positioned to plane), how can I check if the whole surface of a plane is being rendered using orthographic camera?
2. Is it possible to calculate the proportion of a plane's surface that's going go be visible on a render?

The reason I need this is because I cant figure out proper camera positioning function. What I did until now is I: find a center of mesh and calculate the plane's normal out of it, I position the orthographic camera on plane's normal some distance away. Now I don't know how to set the orthographic camera's rotation and scale to render only the plane.

Not finished camera positioning code:

import bpy
import bpy_extras
import mathutils
from mathutils import Vector

def get_global_plane_normal(plane):
normal_local = plane.data.vertices.normal.to_4d()
normal_local.w = 0
normal_global = (plane.matrix_world * normal_local).to_3d()
normal_global.normalize()
return normal_global

def main():
scene = bpy.context.scene
plane = scene.objects.active

coords = [(plane.matrix_world * v.co) for v in plane.data.vertices]

# Calculate the center of the mesh
local_bbox_center = 0.125 * sum((Vector(b) for b in plane.bound_box), Vector())
global_bbox_center = plane.matrix_world * local_bbox_center

plane_normal = get_global_plane_normal(plane)

# Set camera to distance from a plane
distance = 1
camera_position_vtx = global_bbox_center + distance * plane_normal

camera = bpy.context.scene.objects["Camera"]
camera.location = camera_position_vtx
bpy.context.scene.objects.active = camera
# TODO: Rotate and set orthographic scale to the right values,
# only the plane is visible.
main()


camera_fit_coords

The method camera_fit_coords does most of what you need, it returns a location and zoom factor for an ortho camera such that it fits all given coordinates. It does not however rotate the camera, so that will have to be calculated.

Test script, make a plane active, run script and the camera will move to fit the entirety of the plane into the scene camera.

• Calculate the centre of geometry of plane using bound box ( local plane coords)

• Grab the local normal of the first face of plane, I've used bmesh here, could use mesh API instead. Have to use a face normal, a vert normal on a 1 face square plane will be at 45 degrees to x, y axes.

• Find the longest boundary edge to align to the x axis of the camera. At this step could compare the render dimensions of camera and choose to align x or y, if y swap x and y x_axis, y_axis = y_axis, x_axis

• The rotation part of the new global camera matrix is created from the three orthogonal unit vectors, the plane global normal being the -z axis, the x axis aligning with global vector created from longest boundary edge. The other being z_axis.cross(x_axis) Rotation difference could have also been used here.

• Call the camera_fit_coords method with a flat list of all the global coordinates of the planes bounding box, use the return values for location and orthogonal zoom.

import bpy
from mathutils import Vector, Matrix
import bmesh

context = bpy.context
scene = context.scene

cam_ob = scene.camera
plane = context.object
pmw = plane.matrix_world
plane_co = sum((Vector(b) for b in plane.bound_box), Vector()) / 8
me = plane.data
bm = bmesh.new()
bm.from_mesh(me)
bm.normal_update()
bm.faces.ensure_lookup_table()
# first face, could sum them I suppose
bm.faces.normal_update()
plane_no = bm.faces.normal
# longest edge

axis = sorted(
(e for e in bm.edges if e.is_boundary), key=lambda e: e.calc_length(), reverse=True
)

# global coords
z_axis = pmw @ (plane_no) - pmw @ Vector()

x_axis = pmw @ axis.verts.co - pmw @ axis.verts.co

y_axis = z_axis.cross(x_axis)
'''
#  would need to also check scale, hmm bmesh.from_object.. doh.
render = scene.render
if render.resolution_y < render.resolution_x:
x_axis, y_axis = y_axis, x_axis
'''
R = Matrix([v.normalized() for v in (x_axis, y_axis, z_axis)]).transposed()
cam_ob.matrix_world = R.to_4x4()
cam_ob.matrix_world.translation = pmw @ plane_co

scene.update()
coords = [t for b in plane.bound_box for t in pmw @ Vector(b)]

v, scale = cam_ob.camera_fit_coords(context.depsgraph, coords)

cam_ob.data.ortho_scale = scale
cam_ob.matrix_world.translation = v
scene.update()

• There are other questions on here regarding aligning objects with a face normal, or another vector.
• Thank you very much for this code! The only thing that is still bothering me is if I split Cube to 6 planes, and I run this code with vertical "plane" selected, it aligns camera's view with the plane itself, so no surface is in the view. Do you maybe know how to resolve it? – pro Jan 8 '19 at 13:42
• I have "assumed" a regular plane with z normal by using the matrix world of the plane to set the rotation of the camera. Will need to go back to my original idea and use bbox edges and plane normal to make a rotation matrix to get alignment of camera. It's late.. might get around to fix tomorrow. – batFINGER Jan 8 '19 at 13:59
• I create a Cube, select each face -> shortcut P -> Separate selection to its own object. This code works only for 2 faces that are aligned with x,y plane, and for 4 others it's not working properly. I also tried to set origin to gemetry but doesn't seem to help. Also btw how could I look to the other side of the plane? – pro Jan 8 '19 at 14:03
• Yes as I said was assuming it was a default plane where Z local axis is the plane normal. For arbitrary plane will need to calculate the normal (from a face) and use the longest edge or whatever to align with as well. Not difficult.. not doing it now. – batFINGER Jan 8 '19 at 14:07
• Fixed the normal calculation. Only thing now is scale can make the longest edge different. – batFINGER Jan 10 '19 at 11:30

I think it's much more reliable to calculate the exact camera location and rotation instead of that magic trickery with aligning to active. The hardest thing to avoid is the planes that could appear between the plane and the camera. Of course this will imply some 3d math, but it will work in any condition. Like this: find a plane center; detect its normal vector, orientation (where is the width, and where is the height), its maximum (bounding) size. According to camera settings (orthographic scale, resolution) calculate the distance from the plane to cower it entirely; set the camera location and rotatin, using calculated plane center, normal vector.

• Thank you for your answer - it was also my first idea to do that but I got stuck setting cam parameters and than I used those functions. I hide all other planes, so there is no fear of getting other planes in the middle of cam and the plane. I'm able to calculate center of the plane, normal vec, and its orientation, but I have problem with calculating the parameters of the camera and the right distance - is it possible to provide me with some code to calculate these? – pro Jan 8 '19 at 11:32
• it seems that the distance can be anything above zero, only the orthographic scale matters. I'd try to find a coefficient for this scale function from the frame area. First it could be measured for given frame resolution, using a plane. This will give the value to do this cam.ortho_scale = plane_bounding_size * THIS_MAGIC_NUMBER_FOUND_BEFOREHAND; That's all I could invent so far. :-/ – Mechanic Jan 8 '19 at 11:48
• in my case for 640x480 pixels of resolution, and the ortho scale = 1, the plane size is 0.981334 x 0.731689 blender units. And the the relation is linear. So, if I want to cover the plane with width = 3 BU, I have to set ortho scale = 3 / 0.981334 = 3.05706314058211. And it works. And the view doesn't change when I move the camera along its local Z axis. Easy peasy :-) – Mechanic Jan 8 '19 at 12:06