283 lines
9.0 KiB
Python
283 lines
9.0 KiB
Python
# SPDX-FileCopyrightText: 2016-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
bl_info = {
|
|
"name": "Camera Overscan",
|
|
"author": "John Roper, Barnstorm VFX, Luca Scheller, dskjal",
|
|
"version": (1, 4, 2),
|
|
"blender": (3, 1, 0),
|
|
"location": "Render Settings > Camera Overscan",
|
|
"description": "Render Overscan",
|
|
"warning": "",
|
|
"doc_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Render/Camera_Overscan",
|
|
"tracker_url": "",
|
|
"category": "Render"}
|
|
|
|
import bpy
|
|
from bpy.types import (
|
|
Panel,
|
|
Operator,
|
|
PropertyGroup,
|
|
)
|
|
from bpy.props import (
|
|
BoolProperty,
|
|
IntProperty,
|
|
FloatProperty,
|
|
StringProperty,
|
|
PointerProperty,
|
|
)
|
|
|
|
|
|
class RENDER_OT_co_duplicate_camera(Operator):
|
|
bl_idname = "scene.co_duplicate_camera"
|
|
bl_label = "Bake to New Camera"
|
|
bl_description = ("Make a new overscan camera with all the settings builtin\n"
|
|
"Needs an active Camera type in the Scene")
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
active_cam = getattr(context.scene, "camera", None)
|
|
return active_cam is not None
|
|
|
|
def execute(self, context):
|
|
active_cam = getattr(context.scene, "camera", None)
|
|
try:
|
|
if active_cam and active_cam.type == 'CAMERA':
|
|
cam_obj = active_cam.copy()
|
|
cam_obj.data = active_cam.data.copy()
|
|
cam_obj.name = "Camera_Overscan"
|
|
context.collection.objects.link(cam_obj)
|
|
except:
|
|
self.report({'WARNING'}, "Setting up a new Overscan Camera has failed")
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Foldable panel
|
|
class RenderOutputButtonsPanel:
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "output"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return (context.engine in cls.COMPAT_ENGINES)
|
|
|
|
|
|
# UI panel
|
|
class RENDER_PT_overscan(RenderOutputButtonsPanel, Panel):
|
|
bl_label = "Overscan"
|
|
bl_parent_id = "RENDER_PT_format"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
COMPAT_ENGINES = {'CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'}
|
|
|
|
def draw_header(self, context):
|
|
overscan = context.scene.camera_overscan
|
|
self.layout.prop(overscan, "activate", text="")
|
|
|
|
def draw(self, context):
|
|
scene = context.scene
|
|
overscan = scene.camera_overscan
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False # No animation
|
|
|
|
active_cam = getattr(scene, "camera", None)
|
|
|
|
if active_cam and active_cam.type == 'CAMERA':
|
|
col = layout.column(align=True)
|
|
col.prop(overscan, 'original_res_x', text="Original X")
|
|
col.prop(overscan, 'original_res_y', text="Y")
|
|
col.enabled = False
|
|
|
|
col = layout.column(align=True)
|
|
col.prop(overscan, 'custom_res_x', text="New X")
|
|
col.prop(overscan, 'custom_res_y', text="Y")
|
|
col.prop(overscan, 'custom_res_scale', text="%")
|
|
col.enabled = overscan.activate
|
|
|
|
col = layout.column(align=True)
|
|
col.prop(overscan, 'custom_res_offset_x', text="dX")
|
|
col.prop(overscan, 'custom_res_offset_y', text="dY")
|
|
col.prop(overscan, 'custom_res_retain_aspect_ratio', text="Retain Aspect Ratio")
|
|
col.enabled = overscan.activate
|
|
|
|
col = layout.column()
|
|
col.separator()
|
|
col.operator("scene.co_duplicate_camera", icon="RENDER_STILL")
|
|
else:
|
|
layout.label(text="No active camera in the scene", icon='INFO')
|
|
|
|
|
|
def update(self, context):
|
|
scene = context.scene
|
|
overscan = scene.camera_overscan
|
|
render_settings = scene.render
|
|
active_camera = getattr(scene, "camera", None)
|
|
active_cam = getattr(active_camera, "data", None)
|
|
|
|
# Check if there is a camera type in the scene (Object as camera doesn't work)
|
|
if not active_cam or active_camera.type not in {'CAMERA'}:
|
|
return None
|
|
|
|
if overscan.activate:
|
|
if overscan.original_sensor_size == -1:
|
|
# Save property values
|
|
overscan.original_res_x = render_settings.resolution_x
|
|
overscan.original_res_y = render_settings.resolution_y
|
|
overscan.original_sensor_size = active_cam.sensor_width
|
|
overscan.original_sensor_fit = active_cam.sensor_fit
|
|
|
|
if overscan.custom_res_x == 0 or overscan.custom_res_y == 0:
|
|
# Avoid infinite recursion on props update
|
|
if overscan.custom_res_x != render_settings.resolution_x:
|
|
overscan.custom_res_x = render_settings.resolution_x
|
|
if overscan.custom_res_y != render_settings.resolution_y:
|
|
overscan.custom_res_y = render_settings.resolution_y
|
|
|
|
# Reset property values
|
|
active_cam.sensor_width = scene.camera_overscan.original_sensor_size
|
|
|
|
# Calc sensor size
|
|
active_cam.sensor_fit = 'HORIZONTAL'
|
|
dx = overscan.custom_res_offset_x
|
|
dy = overscan.custom_res_offset_y
|
|
scale = overscan.custom_res_scale * 0.01
|
|
x = int(overscan.custom_res_x * scale + dx)
|
|
y = int(overscan.custom_res_y * scale + dy)
|
|
sensor_size_factor = float(x / overscan.original_res_x)
|
|
|
|
# Set new property values
|
|
active_cam.sensor_width = active_cam.sensor_width * sensor_size_factor
|
|
render_settings.resolution_x = x
|
|
render_settings.resolution_y = y
|
|
|
|
else:
|
|
if overscan.original_sensor_size != -1:
|
|
# Restore property values
|
|
render_settings.resolution_x = int(overscan.original_res_x)
|
|
render_settings.resolution_y = int(overscan.original_res_y)
|
|
active_cam.sensor_width = overscan.original_sensor_size
|
|
active_cam.sensor_fit = overscan.original_sensor_fit
|
|
overscan.original_sensor_size = -1
|
|
|
|
|
|
def get_overscan_object(context):
|
|
scene = context.scene
|
|
overscan = scene.camera_overscan
|
|
active_camera = getattr(scene, "camera", None)
|
|
active_cam = getattr(active_camera, "data", None)
|
|
if not active_cam or active_camera.type not in {'CAMERA'} or not overscan.activate:
|
|
return None
|
|
return overscan
|
|
|
|
|
|
def update_x_offset(self, context):
|
|
overscan = get_overscan_object(context)
|
|
if overscan is None:
|
|
return
|
|
|
|
if overscan.custom_res_retain_aspect_ratio:
|
|
overscan.activate = False # Recursion guard
|
|
overscan.custom_res_offset_y = int(overscan.custom_res_offset_x * overscan.original_res_y / overscan.original_res_x)
|
|
|
|
overscan.activate = True
|
|
update(self, context)
|
|
|
|
|
|
def update_y_offset(self, context):
|
|
overscan = get_overscan_object(context)
|
|
if overscan is None:
|
|
return None
|
|
|
|
if overscan.custom_res_retain_aspect_ratio:
|
|
overscan.activate = False # Recursion guard
|
|
overscan.custom_res_offset_x = int(overscan.custom_res_offset_y * overscan.original_res_x / overscan.original_res_y)
|
|
|
|
overscan.activate = True
|
|
update(self, context)
|
|
|
|
|
|
class CameraOverscanProps(PropertyGroup):
|
|
activate: BoolProperty(
|
|
name="Enable Camera Overscan",
|
|
description="Affects the active Scene Camera only\n"
|
|
"(Objects as cameras are not supported)",
|
|
default=False,
|
|
update=update
|
|
)
|
|
custom_res_x: IntProperty(
|
|
name="Target Resolution X",
|
|
default=0,
|
|
min=0,
|
|
max=65536,
|
|
update=update,
|
|
)
|
|
custom_res_y: IntProperty(
|
|
name="Target Resolution Y",
|
|
default=0,
|
|
min=0,
|
|
max=65536,
|
|
update=update,
|
|
)
|
|
custom_res_scale: FloatProperty(
|
|
name="Resolution Percentage",
|
|
default=100,
|
|
min=0,
|
|
max=1000,
|
|
step=100,
|
|
update=update,
|
|
)
|
|
custom_res_offset_x: IntProperty(
|
|
name="Resolution Offset X",
|
|
default=0,
|
|
min=-65536,
|
|
max=65536,
|
|
update=update_x_offset,
|
|
)
|
|
custom_res_offset_y: IntProperty(
|
|
name="Resolution Offset Y",
|
|
default=0,
|
|
min=-65536,
|
|
max=65536,
|
|
update=update_y_offset,
|
|
)
|
|
custom_res_retain_aspect_ratio: BoolProperty(
|
|
name="Retain Aspect Ratio",
|
|
description="Keep the aspect ratio of the original resolution. Affects dX, dY",
|
|
default=False,
|
|
)
|
|
|
|
original_res_x: IntProperty(name="Original Resolution X")
|
|
original_res_y: IntProperty(name="Original Resolution Y")
|
|
|
|
# The hard limit is sys.max which is too much, used 65536 instead
|
|
original_sensor_size: FloatProperty(
|
|
default=-1,
|
|
min=-1,
|
|
max=65536
|
|
)
|
|
original_sensor_fit: StringProperty()
|
|
|
|
|
|
def register():
|
|
bpy.utils.register_class(RENDER_OT_co_duplicate_camera)
|
|
bpy.utils.register_class(CameraOverscanProps)
|
|
bpy.utils.register_class(RENDER_PT_overscan)
|
|
bpy.types.Scene.camera_overscan = PointerProperty(
|
|
type=CameraOverscanProps
|
|
)
|
|
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(RENDER_PT_overscan)
|
|
bpy.utils.unregister_class(RENDER_OT_co_duplicate_camera)
|
|
bpy.utils.unregister_class(CameraOverscanProps)
|
|
del bpy.types.Scene.camera_overscan
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|