688 lines
25 KiB
Python
688 lines
25 KiB
Python
from encodings import utf_8
|
|
import bpy
|
|
import json
|
|
|
|
# import bpy_extras
|
|
from bpy_extras.object_utils import world_to_camera_view # as cam_space
|
|
from bpy_extras.io_utils import ExportHelper
|
|
from mathutils import Vector
|
|
from pathlib import Path
|
|
from . import fn
|
|
|
|
from bpy.props import (
|
|
StringProperty,
|
|
BoolProperty,
|
|
FloatProperty,
|
|
EnumProperty,
|
|
CollectionProperty,
|
|
)
|
|
|
|
from bpy.app.translations import pgettext_data as data_
|
|
|
|
'''
|
|
def Export_AE_2d_position_json_data():
|
|
scn = bpy.context.scene
|
|
cam = scn.objects.get('anim_cam')
|
|
if not cam:
|
|
print('Active camera not "anim_cam"')
|
|
cam = scn.camera
|
|
|
|
rx = scn.render
|
|
rx, ry = rd.resolution_x, rd.resolution_y
|
|
|
|
targets = [o for o in bpy.context.selected_objects if o.type != 'CAMERA']
|
|
|
|
for ob in targets:
|
|
# get an idea of the scale relative to image res ? (too complex since it's not even the right resolution...)
|
|
pos = world_to_camera_view(scn, scn.objects['anim_cam'], ob.matrix_world.to_translation())[:-1]
|
|
pix_pos = Vector((pos[0]*rx, (1-pos[1])*ry))
|
|
'''
|
|
|
|
def export_ae_transforms(directory, selection=None, camera=None, exposition=True, prefix='ae_', suffix='', fr=False, export_format='txt', export_cam=True):
|
|
"""
|
|
Export After Effects transform data for selected objects and camera in Blender.
|
|
|
|
This function exports keyframe data for position, scale, and optional exposition for
|
|
selected objects and the camera. The data can be exported in TXT and/or JSON formats,
|
|
which can be imported into Adobe After Effects.
|
|
|
|
Parameters:
|
|
directory (str): The directory path where the exported files will be saved.
|
|
selection (list, optional): List of Blender objects or pose bones to export. Defaults to None.
|
|
camera (bpy.types.Object, optional): The camera object to use for calculations. Defaults to the scene's active camera.
|
|
exposition (bool, optional): Whether to export exposition data. Defaults to True.
|
|
prefix (str, optional): Prefix for the exported filenames. Defaults to 'ae_'.
|
|
suffix (str, optional): Suffix for the exported filenames. Defaults to ''.
|
|
fr (bool, optional): If True, uses French labels for exposition data. Defaults to False.
|
|
export_format (str, optional): The format(s) to export. Can be 'txt', 'json', or 'txt,json'. Defaults to 'txt'.
|
|
|
|
Example usage:
|
|
export_ae_transforms('/path/to/export', selection=bpy.context.selected_objects, export_format='txt,json')
|
|
"""
|
|
|
|
scn = bpy.context.scene
|
|
res = Vector((scn.render.resolution_x, scn.render.resolution_y))
|
|
|
|
def get_keyframe_data(coords_2d, exposition=False):
|
|
frame_start, org_2d, _ = coords_2d[0]
|
|
org_distance = org_2d[-1]
|
|
|
|
keyframe_data = {'position': [], 'scale': []}
|
|
|
|
for frame, co_2d, is_animated in coords_2d:
|
|
position = Vector((co_2d[0], 1-co_2d[1]))
|
|
position *= res # Multiply by resolution to get pixels value
|
|
keyframe_data['position'].append((frame-frame_start, (round(position[0]), round(position[1]))))
|
|
|
|
scale = (org_distance / co_2d[-1]) * 100
|
|
keyframe_data['scale'].append((frame-frame_start, (round(scale, 5),)*3))
|
|
|
|
if is_animated:
|
|
keyframe_data.setdefault('exposition', []).append(frame-frame_start)
|
|
|
|
return keyframe_data
|
|
|
|
def get_keyframe_data_txt(keyframe_data, exposition=True):
|
|
keyframe_data_txt = (
|
|
'Adobe After Effects 8.0 Keyframe Data\n\n'
|
|
f'\tUnits Per Second\t{scn.render.fps}\n'
|
|
f'\tSource Width\t{scn.render.resolution_x}\n'
|
|
f'\tSource Height\t{scn.render.resolution_y}\n'
|
|
'\tSource Pixel Aspect Ratio\t1\n'
|
|
'\tComp Pixel Aspect Ratio\t1\n\n'
|
|
)
|
|
|
|
# Position
|
|
keyframe_data_txt += (
|
|
'\nTransform\tPosition\n'
|
|
'\tFrame\tX pixels\tY pixels\tZ pixels\t\n'
|
|
)
|
|
for frame, position in keyframe_data['position']:
|
|
keyframe_data_txt += f'\t{frame}\t{position[0]}\t{position[1]}\t0\t\n'
|
|
|
|
# Scale
|
|
keyframe_data_txt += (
|
|
'\nTransform\tScale\n'
|
|
'\tFrame\tX percent\tY percent\tZ percent\t\n'
|
|
)
|
|
for frame, scale in keyframe_data['scale']:
|
|
keyframe_data_txt += f'\t{frame}\t{scale[0]}\t{scale[1]}\t{scale[2]}\t\n'
|
|
|
|
if exposition:
|
|
if fr:
|
|
keyframe_data_txt += '\nEffects\tParamètre case #1\tCase #2\t\n'
|
|
else:
|
|
keyframe_data_txt += '\nEffects\tCheckbox Control #1\tCheckbox #2\t\n'
|
|
|
|
keyframe_data_txt += '\tFrame\t\t\n'
|
|
|
|
for i, frame in enumerate(keyframe_data['exposition']):
|
|
keyframe_data_txt += f'\t{frame}\t{i%2}\t\n'
|
|
|
|
keyframe_data_txt += '\nEnd of Keyframe Data\n'
|
|
|
|
return keyframe_data_txt
|
|
|
|
|
|
if selection is None:
|
|
selection = []
|
|
|
|
if camera is None:
|
|
camera = scn.camera
|
|
|
|
use_simplify = scn.render.use_simplify
|
|
simplify_subdivision = scn.render.simplify_subdivision
|
|
scn.render.use_simplify = True
|
|
scn.render.simplify_subdivision = 0
|
|
|
|
# Get the camera at the first frame for reference
|
|
scn.frame_set(scn.frame_start)
|
|
cam_org_3d = camera.matrix_world @ Vector((0, 0, -100))
|
|
|
|
camera_coords = []
|
|
ob_coords = {}
|
|
animation = {}
|
|
for i in range(scn.frame_start, scn.frame_end + 1):
|
|
scn.frame_set(i)
|
|
|
|
if camera is not None:
|
|
co_2d = world_to_camera_view(scn, camera, cam_org_3d)
|
|
camera_coords.append((i, co_2d, False))
|
|
|
|
for item in selection:
|
|
if isinstance(item, bpy.types.PoseBone):
|
|
co_3d = item.id_data.matrix_world @ item.matrix.to_translation()
|
|
name = f'{item.id_data.name}_{item.name}'
|
|
rig_transforms = [tuple(round(x, 5) for v in b.matrix for x in v) for b in item.id_data.pose.bones]
|
|
is_animated = rig_transforms != animation.get(name)
|
|
animation[name] = rig_transforms
|
|
elif item.type == 'GPENCIL': # isinstance(item.data, bpy.types.GreasePencil):
|
|
name = item.name
|
|
co_3d = item.matrix_world.to_translation()
|
|
## Check if there is a GP-scene at scene-frame (i) on any visible layer
|
|
is_animated = i in [f.frame_number for l in item.data.layers for f in l.frames if not l.hide]
|
|
animation[name] = co_3d
|
|
else:
|
|
name = item.name
|
|
co_3d = item.matrix_world.to_translation()
|
|
is_animated = co_3d != animation.get(name)
|
|
animation[name] = co_3d
|
|
|
|
co_2d = world_to_camera_view(scn, camera, co_3d)
|
|
ob_coords.setdefault(name, []).append((i, co_2d, is_animated))
|
|
|
|
cam_key_frame_data = get_keyframe_data(camera_coords)
|
|
objs_keyframe_data = {name: get_keyframe_data(co, exposition=exposition) for name, co in ob_coords.items()}
|
|
|
|
if 'json' in export_format or export_format == 'json':
|
|
keyframe_data = {
|
|
"units_per_second": scn.render.fps,
|
|
"source_width": scn.render.resolution_x,
|
|
"source_height": scn.render.resolution_y,
|
|
"source_pixel_aspect_ratio": 1,
|
|
"comp_pixel_aspect_ratio": 1,
|
|
'objects': objs_keyframe_data
|
|
}
|
|
if export_cam: #camera is not None:
|
|
keyframe_data['camera'] = cam_key_frame_data
|
|
|
|
Path(directory, f'{prefix}keyframe_data{suffix}.json').write_text(json.dumps(keyframe_data, indent=4), encoding='utf-8')
|
|
|
|
if 'txt' in export_format or export_format == 'txt':
|
|
for name, keyframe_data in objs_keyframe_data.items():
|
|
name = name.replace('.', '_')
|
|
keyframe_data_txt = get_keyframe_data_txt(keyframe_data, exposition=exposition)
|
|
Path(directory, f'{prefix}{name}{suffix}.txt').write_text(keyframe_data_txt, newline='\r\n')
|
|
|
|
if export_cam: #camera is not None:
|
|
keyframe_data_txt = get_keyframe_data_txt(cam_key_frame_data, exposition=False)
|
|
Path(directory, f'{prefix}{camera.name}{suffix}.txt').write_text(keyframe_data_txt, newline='\r\n')
|
|
|
|
scn.render.use_simplify = use_simplify
|
|
scn.render.simplify_subdivision = simplify_subdivision
|
|
|
|
|
|
## Export operator without export_helper
|
|
class GPEXP_OT_export_anim_to_ae(bpy.types.Operator):
|
|
bl_idname = "gp.export_anim_to_ae"
|
|
bl_label = "Export AE Files"
|
|
bl_description = "Export the animation to After Effects, 2D transform of objects, camera\
|
|
\nand/or exposition (including greasepencil frames)"
|
|
bl_options = {"REGISTER"}
|
|
|
|
# filter_glob: StringProperty(default='*.*', options={'HIDDEN'})# *.jpeg;*.png;*.tif;*.tiff;*.bmp
|
|
|
|
# filename_ext = ''
|
|
|
|
# filepath : StringProperty(
|
|
# name="File Path",
|
|
# description="File path used for export",
|
|
# maxlen= 1024)
|
|
|
|
## Only need directory
|
|
directory : StringProperty(
|
|
name="File Path",
|
|
description="File path used for export",
|
|
maxlen= 1024,
|
|
subtype='DIR_PATH'
|
|
)
|
|
prefix : StringProperty(
|
|
name="Prefix",
|
|
default='ae_',
|
|
description="Prefix name for exported txt and json files",
|
|
maxlen= 1024,
|
|
)
|
|
|
|
use_selection: BoolProperty(
|
|
name="Selected Objects",
|
|
description="Export selected and visible objects only",
|
|
default=True,
|
|
)
|
|
use_visible: BoolProperty(
|
|
name='Visible Objects',
|
|
description='Export visible objects only',
|
|
default=False
|
|
)
|
|
use_active_collection: BoolProperty(
|
|
name="Active Collection",
|
|
description="Export only objects from the active collection (and its children)",
|
|
default=False,
|
|
)
|
|
object_types: EnumProperty(
|
|
name="Object Types",
|
|
options={'ENUM_FLAG'},
|
|
items=(('EMPTY', "Empty", ""),
|
|
('CAMERA', "Camera", ""),
|
|
('LIGHT', "Lamp", ""),
|
|
('ARMATURE', "Armature", "WARNING: not supported in dupli/group instances"),
|
|
('MESH', "Mesh", ""),
|
|
('GPENCIL', "Grease Pencil", ""),
|
|
('OTHER', "Other", "Other geometry types, like curve, metaball, etc. (converted to meshes)"),
|
|
),
|
|
description="Which kind of object to export",
|
|
default={'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'GPENCIL', 'OTHER'},
|
|
)
|
|
exposition: BoolProperty(
|
|
name='Exposition',
|
|
description='Export the exposition of the keys',
|
|
default=True
|
|
)
|
|
# use_object_keys: BoolProperty(
|
|
# name='Object Keys',
|
|
# description='Consider object transform keys for animated exposition',
|
|
# default=True
|
|
# )
|
|
use_active_camera: BoolProperty(
|
|
name='Active Camera',
|
|
description='Export active camera keys',
|
|
default=True
|
|
)
|
|
data_lang: EnumProperty(
|
|
name="AE Language",
|
|
items=(('FR', "French", ""),
|
|
('EN', "English", ""),
|
|
),
|
|
description="Clipboard keyframe data language",
|
|
default='FR',
|
|
)
|
|
file_format: EnumProperty(
|
|
name="File Type",
|
|
options={'ENUM_FLAG'},
|
|
items=(('txt', "txt", ""),
|
|
('json', "json", ""),
|
|
),
|
|
description="File format to export (possible to select multiple choices with Shift + Click)",
|
|
default={'txt'},
|
|
)
|
|
|
|
def invoke(self, context, _event):
|
|
if not self.directory:
|
|
blend_filepath = context.blend_data.filepath
|
|
if blend_filepath:
|
|
dest_folder = Path(blend_filepath).parent
|
|
|
|
## If pre-enter a specific subfolder exists
|
|
## (Commented, should be used with a project environment variable)
|
|
# output_folder = dest_folder / 'render'
|
|
# if output_folder.exists() and output_folder.is_dir():
|
|
# dest_folder = output_folder
|
|
|
|
self.directory = str(dest_folder)
|
|
|
|
context.window_manager.fileselect_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False # No animation.
|
|
|
|
# Are we inside the File browser
|
|
is_file_browser = context.space_data.type == 'FILE_BROWSER'
|
|
|
|
export_main(layout, self, is_file_browser)
|
|
export_panel_format(layout, self, is_file_browser)
|
|
export_panel_include(layout, self, is_file_browser)
|
|
|
|
def execute(self, context):
|
|
objects_selection = get_object_selection(use_selection=self.use_selection,
|
|
use_visible=self.use_visible,
|
|
use_active_collection=self.use_active_collection,
|
|
object_types=self.object_types
|
|
)
|
|
|
|
## Ensure output path is directory
|
|
output_path = Path(self.directory)
|
|
if not output_path.is_dir():
|
|
output_path = output_path.parent
|
|
|
|
print('Output directory: ', output_path)
|
|
|
|
cam = None
|
|
if context.scene.camera:
|
|
cam = context.scene.camera
|
|
|
|
if cam and cam in objects_selection:
|
|
## Remove active camera from objects list
|
|
objects_selection.pop(objects_selection.index(cam))
|
|
|
|
print('Export AE transform from objects:')
|
|
for o in objects_selection:
|
|
print('- ', o.name)
|
|
|
|
export_ae_transforms(directory=output_path,
|
|
selection=objects_selection,
|
|
camera=cam,
|
|
exposition=self.exposition,
|
|
prefix=self.prefix,
|
|
fr=self.data_lang == 'FR',
|
|
export_format=self.file_format,
|
|
export_cam=self.use_active_camera)
|
|
|
|
self.report({'INFO'}, f'File(s) saved in folder: {output_path}')
|
|
return {"FINISHED"}
|
|
|
|
|
|
def get_object_selection(use_selection=False, use_visible=False, use_active_collection=False, object_types=None):
|
|
context = bpy.context
|
|
## determine selection based on filters4
|
|
source_collection = None
|
|
if use_active_collection:
|
|
source_collection = context.view_layer.active_layer_collection.collection
|
|
|
|
if source_collection:
|
|
if use_selection:
|
|
ctx_objects = tuple(obj for obj in source_collection.all_objects if obj.select_get())
|
|
else:
|
|
ctx_objects = source_collection.all_objects
|
|
else:
|
|
if use_selection:
|
|
ctx_objects = context.selected_objects
|
|
else:
|
|
ctx_objects = context.view_layer.objects
|
|
|
|
if use_visible:
|
|
ctx_objects = tuple(obj for obj in ctx_objects if obj.visible_get())
|
|
|
|
## Filter by object type
|
|
if object_types is None:
|
|
object_types = {'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'GPENCIL', 'OTHER'}
|
|
|
|
ctx_objects = [obj for obj in ctx_objects if obj.type in object_types]
|
|
## Constant for compatible other object type
|
|
# BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'}
|
|
|
|
if 'OTHER' in object_types:
|
|
## Any object that is not in proposed list
|
|
ctx_objects += [obj for obj in ctx_objects if obj.type not in object_types]
|
|
|
|
return ctx_objects
|
|
|
|
|
|
def export_main(layout, operator, is_file_browser):
|
|
col = layout.column()
|
|
col.prop(operator, 'exposition')
|
|
col.prop(operator, 'use_active_camera')
|
|
# col.prop(operator, 'use_object_keys')
|
|
|
|
def export_panel_format(layout, operator, is_file_browser):
|
|
header, body = layout.panel("AE_export_format", default_closed=False)
|
|
header.label(text="Format")
|
|
if body:
|
|
col = body.column()
|
|
col.prop(operator, 'prefix')
|
|
## Format (language and file)
|
|
|
|
col.row().prop(operator, 'data_lang', expand=True)
|
|
if is_file_browser:
|
|
col.column().prop(operator, 'file_format')
|
|
|
|
def export_panel_include(layout, operator, is_file_browser):
|
|
header, body = layout.panel("AE_export_include", default_closed=False)
|
|
header.label(text="Include")
|
|
if body:
|
|
sublayout = body.column(heading="Limit to")
|
|
# if is_file_browser:
|
|
sublayout.prop(operator, "use_selection")
|
|
sublayout.prop(operator, "use_visible")
|
|
sublayout.prop(operator, "use_active_collection")
|
|
|
|
body.column().prop(operator, "object_types")
|
|
# body.prop(operator, "use_custom_props")
|
|
|
|
### --- Old functions
|
|
|
|
# Unused (old func that might not be usefull at all...)
|
|
def correct_shift(vec, cam):
|
|
resX = bpy.context.scene.render.resolution_x
|
|
resY = bpy.context.scene.render.resolution_y
|
|
ratio = resX/resY
|
|
shiftX = 2*cam.data.shift_x
|
|
shiftY = 2*cam.data.shift_y
|
|
|
|
if ratio<1:
|
|
return vec - Vector((shiftX*(1/ratio), shiftY, 0))
|
|
elif ratio>1:
|
|
return vec - Vector((shiftX, shiftY*ratio, 0))
|
|
else:
|
|
return vec - Vector((shiftX, shiftY, 0))
|
|
|
|
def export_AE_objects_position_keys():
|
|
'''Export keys as paperclip to paste in after'''
|
|
|
|
scn = bpy.context.scene
|
|
result = {}
|
|
print(f'Exporting 2d position (scene range: {scn.frame_start} - {scn.frame_end})')
|
|
for fr in range(scn.frame_start,scn.frame_end + 1):
|
|
|
|
print(f'frame: {fr}')
|
|
scn.frame_set(fr)
|
|
|
|
for o in bpy.context.selected_objects:
|
|
if not result.get(o.name):
|
|
result[o.name] = []
|
|
proj2d = world_to_camera_view(scn, scn.camera, o.matrix_world.to_translation()) # + Vector((.5,.5,0))
|
|
|
|
# proj2d = correct_shift(proj2d, scn.camera) # needed ?
|
|
x = (proj2d[0]) * scn.render.resolution_x
|
|
y = -(proj2d[1]) * scn.render.resolution_y + scn.render.resolution_y
|
|
|
|
result[o.name].append((fr,x,y))
|
|
|
|
for name,value in result.items():
|
|
|
|
txt = fn.get_ae_keyframe_clipboard_header(scn)
|
|
|
|
for v in value:
|
|
txt += '\t%s\t%s\t%s\t0\t\n'%(v[0],v[1],v[2]) # add 0 for Z (probably not needed)
|
|
|
|
txt += '\n\nEnd of Keyframe Data\n' # keyframe txt footer
|
|
|
|
blend = Path(bpy.data.filepath)
|
|
keyfile = blend.parent / 'render' / f'pos_{name}.txt'
|
|
keyfile.parent.mkdir(parents=False, exist_ok=True)
|
|
|
|
print(f'exporting keys for {name} at {keyfile}')
|
|
|
|
## save forcing CRLF terminator (DOS style, damn windows)
|
|
## in case it's exported from linux
|
|
with open(keyfile, 'w', newline='\r\n') as fd:
|
|
fd.write(txt)
|
|
|
|
|
|
class GPEXP_OT_export_keys_to_ae(bpy.types.Operator):
|
|
bl_idname = "gp.export_keys_to_ae"
|
|
bl_label = "Export 2D Position To AE"
|
|
bl_description = "Export selected objects positions as text file containing key paperclip for AfterEffects layers"
|
|
bl_options = {"REGISTER"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.selected_objects
|
|
|
|
def execute(self, context):
|
|
export_AE_objects_position_keys()
|
|
return {"FINISHED"}
|
|
|
|
|
|
def export_anim_cam_position(camera=None, context=None):
|
|
context = context or bpy.context
|
|
scn = context.scene
|
|
|
|
camera = camera or bpy.data.objects.get('anim_cam')
|
|
if not camera:
|
|
return 'Abort: No "anim_cam" found!'
|
|
|
|
text = fn.get_ae_keyframe_clipboard_header(scn)
|
|
for i in range(scn.frame_start, scn.frame_end + 1):
|
|
scn.frame_set(i)
|
|
center = fn.get_cam_frame_center_world(camera)
|
|
coord = fn.get_coord_in_cam_space(scn, scn.camera, center, ae=True)
|
|
text += f'\t{i}\t{coord[0]}\t{coord[1]}\t\n'
|
|
|
|
text += '\n\nEnd of Keyframe Data\n' # Ae Frame ending
|
|
|
|
blend = Path(bpy.data.filepath)
|
|
keyfile = blend.parent / 'render' / f'anim_cam_pos.txt'
|
|
keyfile.parent.mkdir(parents=False, exist_ok=True)
|
|
|
|
print(f'Exporting anim cam positions keys at: {keyfile}')
|
|
with open(keyfile, 'w') as fd:
|
|
fd.write(text)
|
|
|
|
class GPEXP_OT_export_cam_keys_to_ae(bpy.types.Operator):
|
|
bl_idname = "gp.export_cam_keys_to_ae"
|
|
bl_label = "Export Camera 2D Position To AE"
|
|
bl_description = "Export anim cam positions as text file containing key paperclip for AfterEffects layers"
|
|
bl_options = {"REGISTER"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.selected_objects
|
|
|
|
def execute(self, context):
|
|
cam = None
|
|
if context.object and context.object.type == 'CAMERA' and context.object != context.scene.camera:
|
|
cam = context.object
|
|
|
|
err = export_anim_cam_position(camera=cam, context=context)
|
|
if err:
|
|
self.report({'ERROR'}, err)
|
|
return {"CANCELLED"}
|
|
return {"FINISHED"}
|
|
|
|
class GPEXP_OT_fix_overscan_shift(bpy.types.Operator):
|
|
bl_idname = "gp.fix_overscan_shift"
|
|
bl_label = "Fix Cam Shift Value With Overscan"
|
|
bl_description = "(Gp render operator) change shift values to re-center overscan"
|
|
bl_options = {"REGISTER"}
|
|
|
|
# @classmethod
|
|
# def poll(cls, context):
|
|
# return context.object
|
|
|
|
init_rx : bpy.props.IntProperty(name='pre-overscan res x')
|
|
init_ry : bpy.props.IntProperty(name='pre-overscan res y')
|
|
|
|
def invoke(self, context, event):
|
|
# if not context.object:
|
|
# self.report({'ERROR'}, 'No object selected')
|
|
# return {'CANCELLED'}
|
|
|
|
self.use_selection = False
|
|
if context.active_object and context.active_object.type == 'CAMERA' and context.active_object != context.scene.camera:
|
|
self.use_selection = True
|
|
self.cam_ob = context.active_object
|
|
else:
|
|
self.cam_ob = context.scene.camera
|
|
|
|
self.init_rx = context.scene.render.resolution_x
|
|
self.init_ry = context.scene.render.resolution_y
|
|
|
|
return context.window_manager.invoke_props_dialog(self)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
if self.use_selection:
|
|
col = layout.column()
|
|
col.label(text=f'Camera "{self.cam_ob.name}" selected', icon='INFO')
|
|
col.label(text='Change in shifts will apply on this one', icon='BLANK1')
|
|
|
|
col = layout.column()
|
|
col.label(text='Overscan res (current)')
|
|
col.label(text=f'{context.scene.render.resolution_x} x {context.scene.render.resolution_y}')
|
|
|
|
col = layout.column()
|
|
col.label(text='Enter Initial (pre-overscan) resolution:')
|
|
row = col.row(align=True)
|
|
row.prop(self, 'init_rx', text='')
|
|
row.prop(self, 'init_ry', text='')
|
|
|
|
def execute(self, context):
|
|
cam = self.cam_ob.data
|
|
|
|
ratio_x = self.init_rx / context.scene.render.resolution_x
|
|
ratio_y = self.init_ry / context.scene.render.resolution_y
|
|
|
|
if ratio_x == 1 and ratio_y == 1:
|
|
self.report({'ERROR'}, 'Same init and overscan resolution, nothing to change')
|
|
return {'CANCELLED'}
|
|
|
|
if ratio_x != 1:
|
|
if fn.has_keyframe(cam, 'shift_x'):
|
|
fcu = cam.animation_data.action.fcurves.find('shift_x')
|
|
for k in fcu.keyframe_points:
|
|
k.co.y = k.co.y * ratio_x
|
|
else:
|
|
if cam.shift_x != 1:
|
|
cam.shift_x = cam.shift_x * ratio_x
|
|
|
|
if ratio_y != 1:
|
|
if fn.has_keyframe(cam, 'shift_y'):
|
|
fcu = cam.animation_data.action.fcurves.find('shift_y')
|
|
for k in fcu.keyframe_points:
|
|
k.co.y = k.co.y * ratio_y
|
|
else:
|
|
if cam.shift_y != 1:
|
|
cam.shift_y = cam.shift_y * ratio_y
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
# ui panel
|
|
class GPEXP_PT_extra_gprender_func(bpy.types.Panel):
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "output"
|
|
bl_label = "GP Render Extras"
|
|
bl_parent_id = "RENDER_PT_format" if (3, 0, 0) <= bpy.app.version else "RENDER_PT_dimensions"
|
|
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, "RO_Activate", text="")
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
col = layout.column()
|
|
col.operator("gp.fix_overscan_shift")
|
|
col.operator("gp.export_keys_to_ae")
|
|
col.operator("gp.export_cam_keys_to_ae")
|
|
|
|
# def overcan_shift_fix_ui(self, context):
|
|
# layout = self.layout
|
|
# layout.operator("gp.fix_overscan_shift")
|
|
|
|
def export_ae_anim_menu(self, context):
|
|
row = self.layout.row(align=False)
|
|
row.operator('gp.export_anim_to_ae', text='After Effects Keyframe Data (.txt/.json)')
|
|
# row.operator('gp.export_anim_to_ae', text='', icon='COPYDOWN')
|
|
|
|
|
|
classes=(
|
|
GPEXP_OT_export_keys_to_ae,
|
|
GPEXP_OT_export_cam_keys_to_ae,
|
|
GPEXP_OT_fix_overscan_shift,
|
|
GPEXP_PT_extra_gprender_func,
|
|
GPEXP_OT_export_anim_to_ae,
|
|
)
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
|
|
bpy.types.TOPBAR_MT_file_export.append(export_ae_anim_menu)
|
|
# if hasattr(bpy.types, 'RENDER_PT_overscan'):
|
|
# bpy.types.RENDER_PT_overscan.append(overcan_shift_fix_ui)
|
|
|
|
|
|
def unregister():
|
|
# if hasattr(bpy.types, 'RENDER_PT_overscan'):
|
|
# bpy.types.RENDER_PT_overscan.remove(overcan_shift_fix_ui)
|
|
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|
|
bpy.types.TOPBAR_MT_file_export.remove(export_ae_anim_menu)
|