@@ -57,7 +57,7 @@ class Axes3D(Axes):
5757 def __init__ (
5858 self , fig , rect = None , * args ,
5959 elev = 30 , azim = - 60 , roll = 0 , sharez = None , proj_type = 'persp' ,
60- box_aspect = None , computed_zorder = True ,
60+ box_aspect = None , computed_zorder = True , focal_length = None ,
6161 ** kwargs ):
6262 """
6363 Parameters
@@ -104,6 +104,13 @@ def __init__(
104104 This behavior is deprecated in 3.4, the default will
105105 change to False in 3.5. The keyword will be undocumented
106106 and a non-False value will be an error in 3.6.
107+ focal_length : float, default: None
108+ For a projection type of 'persp', the focal length of the virtual
109+ camera. Must be > 0. If None, defaults to 1.
110+ For a projection type of 'ortho', must be set to either None
111+ or infinity (numpy.inf). If None, defaults to infinity.
112+ The focal length can be computed from a desired Field Of View via
113+ the equation: focal_length = 1/tan(FOV/2)
107114
108115 **kwargs
109116 Other optional keyword arguments:
@@ -117,7 +124,7 @@ def __init__(
117124 self .initial_azim = azim
118125 self .initial_elev = elev
119126 self .initial_roll = roll
120- self .set_proj_type (proj_type )
127+ self .set_proj_type (proj_type , focal_length )
121128 self .computed_zorder = computed_zorder
122129
123130 self .xy_viewLim = Bbox .unit ()
@@ -989,18 +996,33 @@ def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z"):
989996 dict (x = 0 , y = 1 , z = 2 ), vertical_axis = vertical_axis
990997 )
991998
992- def set_proj_type (self , proj_type ):
999+ def set_proj_type (self , proj_type , focal_length = None ):
9931000 """
9941001 Set the projection type.
9951002
9961003 Parameters
9971004 ----------
9981005 proj_type : {'persp', 'ortho'}
999- """
1000- self ._projection = _api .check_getitem ({
1001- 'persp' : proj3d .persp_transformation ,
1002- 'ortho' : proj3d .ortho_transformation ,
1003- }, proj_type = proj_type )
1006+ The projection type.
1007+ focal_length : float, default: None
1008+ For a projection type of 'persp', the focal length of the virtual
1009+ camera. Must be > 0. If None, defaults to 1.
1010+ The focal length can be computed from a desired Field Of View via
1011+ the equation: focal_length = 1/tan(FOV/2)
1012+ """
1013+ _api .check_in_list (['persp' , 'ortho' ], proj_type = proj_type )
1014+ if proj_type == 'persp' :
1015+ if focal_length is None :
1016+ focal_length = 1
1017+ elif focal_length <= 0 :
1018+ raise ValueError (f"focal_length = { focal_length } must be "
1019+ "greater than 0" )
1020+ self ._focal_length = focal_length
1021+ elif proj_type == 'ortho' :
1022+ if focal_length not in (None , np .inf ):
1023+ raise ValueError (f"focal_length = { focal_length } must be "
1024+ f"None for proj_type = { proj_type } " )
1025+ self ._focal_length = np .inf
10041026
10051027 def _roll_to_vertical (self , arr ):
10061028 """Roll arrays to match the different vertical axis."""
@@ -1056,8 +1078,21 @@ def get_proj(self):
10561078 V = np .zeros (3 )
10571079 V [self ._vertical_axis ] = - 1 if abs (elev_rad ) > 0.5 * np .pi else 1
10581080
1059- viewM = proj3d .view_transformation (eye , R , V , roll_rad )
1060- projM = self ._projection (- self ._dist , self ._dist )
1081+ # Generate the view and projection transformation matrices
1082+ if self ._focal_length == np .inf :
1083+ # Orthographic projection
1084+ viewM = proj3d .view_transformation (eye , R , V , roll_rad )
1085+ projM = proj3d .ortho_transformation (- self ._dist , self ._dist )
1086+ else :
1087+ # Perspective projection
1088+ # Scale the eye dist to compensate for the focal length zoom effect
1089+ eye_focal = R + self ._dist * ps * self ._focal_length
1090+ viewM = proj3d .view_transformation (eye_focal , R , V , roll_rad )
1091+ projM = proj3d .persp_transformation (- self ._dist ,
1092+ self ._dist ,
1093+ self ._focal_length )
1094+
1095+ # Combine all the transformation matrices to get the final projection
10611096 M0 = np .dot (viewM , worldM )
10621097 M = np .dot (projM , M0 )
10631098 return M
@@ -1120,7 +1155,7 @@ def cla(self):
11201155 pass
11211156
11221157 self ._autoscaleZon = True
1123- if self ._projection is proj3d . ortho_transformation :
1158+ if self ._focal_length == np . inf :
11241159 self ._zmargin = rcParams ['axes.zmargin' ]
11251160 else :
11261161 self ._zmargin = 0.
0 commit comments