UI: Move Extensions repositories popover to header
[blender-addons-contrib.git] / camera_overscan.py
blob1ddee7596a79757c457b6d3573d5c3e2a5d732ec
1 # SPDX-FileCopyrightText: 2016-2023 Blender Authors
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 bl_info = {
6 "name": "Camera Overscan",
7 "author": "John Roper, Barnstorm VFX, Luca Scheller, dskjal",
8 "version": (1, 4, 2),
9 "blender": (3, 1, 0),
10 "location": "Render Settings > Camera Overscan",
11 "description": "Render Overscan",
12 "warning": "",
13 "doc_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Render/Camera_Overscan",
14 "tracker_url": "",
15 "category": "Render"}
17 import bpy
18 from bpy.types import (
19 Panel,
20 Operator,
21 PropertyGroup,
23 from bpy.props import (
24 BoolProperty,
25 IntProperty,
26 FloatProperty,
27 StringProperty,
28 PointerProperty,
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")
38 @classmethod
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)
45 try:
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)
51 except:
52 self.report({'WARNING'}, "Setting up a new Overscan Camera has failed")
53 return {'CANCELLED'}
55 return {'FINISHED'}
58 # Foldable panel
59 class RenderOutputButtonsPanel:
60 bl_space_type = 'PROPERTIES'
61 bl_region_type = 'WINDOW'
62 bl_context = "output"
64 @classmethod
65 def poll(cls, context):
66 return (context.engine in cls.COMPAT_ENGINES)
69 # UI panel
70 class RENDER_PT_overscan(RenderOutputButtonsPanel, Panel):
71 bl_label = "Overscan"
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):
81 scene = context.scene
82 overscan = scene.camera_overscan
83 layout = self.layout
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")
93 col.enabled = False
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()
108 col.separator()
109 col.operator("scene.co_duplicate_camera", icon="RENDER_STILL")
110 else:
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'}:
123 return None
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
143 # Calc 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
157 else:
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:
173 return None
174 return overscan
177 def update_x_offset(self, context):
178 overscan = get_overscan_object(context)
179 if overscan is None:
180 return
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)
192 if overscan is None:
193 return None
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)",
208 default=False,
209 update=update
211 custom_res_x: IntProperty(
212 name="Target Resolution X",
213 default=0,
214 min=0,
215 max=65536,
216 update=update,
218 custom_res_y: IntProperty(
219 name="Target Resolution Y",
220 default=0,
221 min=0,
222 max=65536,
223 update=update,
225 custom_res_scale: FloatProperty(
226 name="Resolution Percentage",
227 default=100,
228 min=0,
229 max=1000,
230 step=100,
231 update=update,
233 custom_res_offset_x: IntProperty(
234 name="Resolution Offset X",
235 default=0,
236 min=-65536,
237 max=65536,
238 update=update_x_offset,
240 custom_res_offset_y: IntProperty(
241 name="Resolution Offset Y",
242 default=0,
243 min=-65536,
244 max=65536,
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",
250 default=False,
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(
258 default=-1,
259 min=-1,
260 max=65536
262 original_sensor_fit: StringProperty()
265 def register():
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
274 def unregister():
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__":
282 register()