1 # SPDX-FileCopyrightText: 2016-2023 Blender Authors
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 "name": "Camera Overscan",
7 "author": "John Roper, Barnstorm VFX, Luca Scheller, dskjal",
10 "location": "Render Settings > Camera Overscan",
11 "description": "Render Overscan",
13 "doc_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Render/Camera_Overscan",
18 from bpy
.types
import (
23 from bpy
.props
import (
32 class RENDER_OT_co_duplicate_camera(Operator
):
33 bl_idname
= "scene.co_duplicate_camera"
34 bl_label
= "Bake to New Camera"
35 bl_description
= ("Make a new overscan camera with all the settings builtin\n"
36 "Needs an active Camera type in the Scene")
39 def poll(cls
, context
):
40 active_cam
= getattr(context
.scene
, "camera", None)
41 return active_cam
is not None
43 def execute(self
, context
):
44 active_cam
= getattr(context
.scene
, "camera", None)
46 if active_cam
and active_cam
.type == 'CAMERA':
47 cam_obj
= active_cam
.copy()
48 cam_obj
.data
= active_cam
.data
.copy()
49 cam_obj
.name
= "Camera_Overscan"
50 context
.collection
.objects
.link(cam_obj
)
52 self
.report({'WARNING'}, "Setting up a new Overscan Camera has failed")
59 class RenderOutputButtonsPanel
:
60 bl_space_type
= 'PROPERTIES'
61 bl_region_type
= 'WINDOW'
65 def poll(cls
, context
):
66 return (context
.engine
in cls
.COMPAT_ENGINES
)
70 class RENDER_PT_overscan(RenderOutputButtonsPanel
, Panel
):
72 bl_parent_id
= "RENDER_PT_format"
73 bl_options
= {'DEFAULT_CLOSED'}
74 COMPAT_ENGINES
= {'CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'}
76 def draw_header(self
, context
):
77 overscan
= context
.scene
.camera_overscan
78 self
.layout
.prop(overscan
, "activate", text
="")
80 def draw(self
, context
):
82 overscan
= scene
.camera_overscan
84 layout
.use_property_split
= True
85 layout
.use_property_decorate
= False # No animation
87 active_cam
= getattr(scene
, "camera", None)
89 if active_cam
and active_cam
.type == 'CAMERA':
90 col
= layout
.column(align
=True)
91 col
.prop(overscan
, 'original_res_x', text
="Original X")
92 col
.prop(overscan
, 'original_res_y', text
="Y")
95 col
= layout
.column(align
=True)
96 col
.prop(overscan
, 'custom_res_x', text
="New X")
97 col
.prop(overscan
, 'custom_res_y', text
="Y")
98 col
.prop(overscan
, 'custom_res_scale', text
="%")
99 col
.enabled
= overscan
.activate
101 col
= layout
.column(align
=True)
102 col
.prop(overscan
, 'custom_res_offset_x', text
="dX")
103 col
.prop(overscan
, 'custom_res_offset_y', text
="dY")
104 col
.prop(overscan
, 'custom_res_retain_aspect_ratio', text
="Retain Aspect Ratio")
105 col
.enabled
= overscan
.activate
107 col
= layout
.column()
109 col
.operator("scene.co_duplicate_camera", icon
="RENDER_STILL")
111 layout
.label(text
="No active camera in the scene", icon
='INFO')
114 def update(self
, context
):
115 scene
= context
.scene
116 overscan
= scene
.camera_overscan
117 render_settings
= scene
.render
118 active_camera
= getattr(scene
, "camera", None)
119 active_cam
= getattr(active_camera
, "data", None)
121 # Check if there is a camera type in the scene (Object as camera doesn't work)
122 if not active_cam
or active_camera
.type not in {'CAMERA'}:
125 if overscan
.activate
:
126 if overscan
.original_sensor_size
== -1:
127 # Save property values
128 overscan
.original_res_x
= render_settings
.resolution_x
129 overscan
.original_res_y
= render_settings
.resolution_y
130 overscan
.original_sensor_size
= active_cam
.sensor_width
131 overscan
.original_sensor_fit
= active_cam
.sensor_fit
133 if overscan
.custom_res_x
== 0 or overscan
.custom_res_y
== 0:
134 # Avoid infinite recursion on props update
135 if overscan
.custom_res_x
!= render_settings
.resolution_x
:
136 overscan
.custom_res_x
= render_settings
.resolution_x
137 if overscan
.custom_res_y
!= render_settings
.resolution_y
:
138 overscan
.custom_res_y
= render_settings
.resolution_y
140 # Reset property values
141 active_cam
.sensor_width
= scene
.camera_overscan
.original_sensor_size
144 active_cam
.sensor_fit
= 'HORIZONTAL'
145 dx
= overscan
.custom_res_offset_x
146 dy
= overscan
.custom_res_offset_y
147 scale
= overscan
.custom_res_scale
* 0.01
148 x
= int(overscan
.custom_res_x
* scale
+ dx
)
149 y
= int(overscan
.custom_res_y
* scale
+ dy
)
150 sensor_size_factor
= float(x
/ overscan
.original_res_x
)
152 # Set new property values
153 active_cam
.sensor_width
= active_cam
.sensor_width
* sensor_size_factor
154 render_settings
.resolution_x
= x
155 render_settings
.resolution_y
= y
158 if overscan
.original_sensor_size
!= -1:
159 # Restore property values
160 render_settings
.resolution_x
= int(overscan
.original_res_x
)
161 render_settings
.resolution_y
= int(overscan
.original_res_y
)
162 active_cam
.sensor_width
= overscan
.original_sensor_size
163 active_cam
.sensor_fit
= overscan
.original_sensor_fit
164 overscan
.original_sensor_size
= -1
167 def get_overscan_object(context
):
168 scene
= context
.scene
169 overscan
= scene
.camera_overscan
170 active_camera
= getattr(scene
, "camera", None)
171 active_cam
= getattr(active_camera
, "data", None)
172 if not active_cam
or active_camera
.type not in {'CAMERA'} or not overscan
.activate
:
177 def update_x_offset(self
, context
):
178 overscan
= get_overscan_object(context
)
182 if overscan
.custom_res_retain_aspect_ratio
:
183 overscan
.activate
= False # Recursion guard
184 overscan
.custom_res_offset_y
= int(overscan
.custom_res_offset_x
* overscan
.original_res_y
/ overscan
.original_res_x
)
186 overscan
.activate
= True
187 update(self
, context
)
190 def update_y_offset(self
, context
):
191 overscan
= get_overscan_object(context
)
195 if overscan
.custom_res_retain_aspect_ratio
:
196 overscan
.activate
= False # Recursion guard
197 overscan
.custom_res_offset_x
= int(overscan
.custom_res_offset_y
* overscan
.original_res_x
/ overscan
.original_res_y
)
199 overscan
.activate
= True
200 update(self
, context
)
203 class CameraOverscanProps(PropertyGroup
):
204 activate
: BoolProperty(
205 name
="Enable Camera Overscan",
206 description
="Affects the active Scene Camera only\n"
207 "(Objects as cameras are not supported)",
211 custom_res_x
: IntProperty(
212 name
="Target Resolution X",
218 custom_res_y
: IntProperty(
219 name
="Target Resolution Y",
225 custom_res_scale
: FloatProperty(
226 name
="Resolution Percentage",
233 custom_res_offset_x
: IntProperty(
234 name
="Resolution Offset X",
238 update
=update_x_offset
,
240 custom_res_offset_y
: IntProperty(
241 name
="Resolution Offset Y",
245 update
=update_y_offset
,
247 custom_res_retain_aspect_ratio
: BoolProperty(
248 name
="Retain Aspect Ratio",
249 description
="Keep the aspect ratio of the original resolution. Affects dX, dY",
253 original_res_x
: IntProperty(name
="Original Resolution X")
254 original_res_y
: IntProperty(name
="Original Resolution Y")
256 # The hard limit is sys.max which is too much, used 65536 instead
257 original_sensor_size
: FloatProperty(
262 original_sensor_fit
: StringProperty()
266 bpy
.utils
.register_class(RENDER_OT_co_duplicate_camera
)
267 bpy
.utils
.register_class(CameraOverscanProps
)
268 bpy
.utils
.register_class(RENDER_PT_overscan
)
269 bpy
.types
.Scene
.camera_overscan
= PointerProperty(
270 type=CameraOverscanProps
275 bpy
.utils
.unregister_class(RENDER_PT_overscan
)
276 bpy
.utils
.unregister_class(RENDER_OT_co_duplicate_camera
)
277 bpy
.utils
.unregister_class(CameraOverscanProps
)
278 del bpy
.types
.Scene
.camera_overscan
281 if __name__
== "__main__":