# SPDX-License-Identifier: GPL-3.0-or-later ## Standalone based on SPA code ## Code from create_gp_texture_ref.py (Copyright (C) 2023, The SPA Studios. All rights reserved.) ## adapted to create as a single 'GP as image' and fit in camera with driver import bpy from bpy_extras.io_utils import ImportHelper from bpy.props import (StringProperty, CollectionProperty, BoolProperty, EnumProperty) import json import bpy_extras from mathutils import Vector import os from pathlib import Path from .. import fn from .. constants import * from .. export_psd_layers import export_psd_bg def get_json_infos(json_path) -> tuple((list, tuple)): '''return a tuple with (image paths list, [x, y] original psd resolution''' import json setup_json_path = Path(json_path) setup_json = json.loads(setup_json_path.read_text()) # psd = Path(setup_json['psd']) # path to psd # image_size = setup_json['image_size'] # image size (reduced) # png_dir = Path(setup_json['png_dir']) # path to png directory layers = setup_json['layers'] # dic : name, lbbox, bbox, index, path org_image_size = setup_json['org_image_size'] # original PSD dimensions img_list = [Path(l['path']) for l in layers] # (png_dir /l['name']).with_suffix('.png') return img_list, org_image_size def setup_bg_camera(image_size, context=None, scn=None, cam_type=None): '''Create background camera from scene active camera according to background image resolution image_size :: cam_type :: wether the BG camera should be set to perspective or orthographic ''' context = context or bpy.context scn=scn or context.scene movie_resolution = scn.render.resolution_x, scn.render.resolution_y anim_cam = scn.camera anim_cam_scale = 6 # anim_cam.data.ortho_scale anim_cam_focale = 50 # anim_cam.data.lens cam_type = cam_type or anim_cam.data.type ## scale up plane scale and bg_cam ortho_scale according to film res bg_factor = (anim_cam_scale * image_size[0]) / movie_resolution[0] #print(f'({anim_cam_scale} * {image_size[0]}) / {movie_resolution[0]} = {bg_factor}') ## scale down the focal according to film res bg_focale = (anim_cam_focale * movie_resolution[0]) / image_size[0] #print(f'({anim_cam_focale} * {movie_resolution[0]}) / {image_size[0]} = {bg_focale}') bg_cam = scn.objects.get('bg_cam') if not bg_cam: bg_cam = fn.create_cam(name='bg_cam', type=cam_type, size=bg_factor, focale=bg_focale) bg_cam['resolution'] = image_size bg_cam.matrix_world = anim_cam.matrix_world bg_cam.hide_select = True bg_cam.lock_location = bg_cam.lock_rotation = bg_cam.lock_scale = [True]*3 fn.set_collection(bg_cam, 'Camera') fn.set_collection(anim_cam, 'Camera') return bg_cam class BPM_OT_import_bg_images(bpy.types.Operator, ImportHelper): bl_idname = "bpm.import_bg_images" bl_label = "Import Background images" bl_description = "Import either a Json, a PSD, multiple files" bl_options = {"REGISTER"} # , "INTERNAL" # filename_ext = '.json' filter_glob: StringProperty( default=';'.join([f'*{i}' for i in bpy.path.extensions_image]) + ';*.json', options={'HIDDEN'} ) ## Active selection filepath : StringProperty( name="File Path", description="File path used for import", maxlen= 1024) # the active file ## Handle multi-selection files: CollectionProperty( name="File Path", type=bpy.types.OperatorFileListElement, ) # The filelist collection ## Choice to place before or after ? import_type : EnumProperty( name="Import As", description="Type of import to ", default='GPENCIL', options={'ANIMATABLE'}, items=( ('GPENCIL', 'Gpencil Object', 'Import bg planes as gpencil objects', 0), ('EMPTY', 'Empty Reference', 'Import bg planes as empty objects', 1), ('MESH', 'Texture Plane', 'Import bg planes as mesh objects', 2), )) mode : EnumProperty( name="Mode", description="", default='REPLACE', options={'ANIMATABLE'}, items=( ('REPLACE', 'Replace Existing', 'Replace the image if already exists', 0), ('SKIP', 'Skip Existing', 'Skip the import if the image alreaady exists in planes', 1), # ('PURGE', 'Purge', 'When object exists, fully delete it before linking, even associated collection and holder', 2), )) def execute(self, context): org_image_size = None scn = context.scene active_file = Path(self.filepath) if len(self.files) == 1 and active_file.suffix.lower() in ('.json', '.psd'): json_path = None print('active_file.suffix.lower(): ', active_file.suffix.lower()) if active_file.suffix.lower() == '.psd': print('Is a PSD') ## Export layers and create json setup file # Export passes in a 'render' or 'render_hd' folder aside psd json_path = export_psd_bg(str(active_file)) elif active_file.suffix.lower() == '.json': print('Is a json') # Use json data to batch import json_path = active_file if not json_path: self.report({'ERROR'}, 'No json path to load, you can try loading from psd or selecting image file directly') return {'CANCELLED'} file_list, org_image_size = get_json_infos(json_path) folder = Path(json_path).parent # render folder else: folder = active_file.parent # Filter out json (we may want ot import the PSD as imagelisted in bpy.path.extensions_image) file_list = [folder / f.name for f in self.files if Path(f.name).suffix in bpy.path.extensions_image] if not file_list: self.report({'ERROR'}, 'Image file list is empty') return {'CANCELLED'} ## simple_list # existing_backgrounds = [o for o in context.scene.objects if o.get('is_background')] # existing_holders = [o for o in context.scene.objects if o.name.startswith(PREFIX) and o.type == 'MESH' and o.children] ## Has dict # existing_backgrounds = {o : img_info for o in context.scene.objects if o.get('is_background') and (img_info := o.get('is_background'))} # FIXME: Use existing background custom prop? : o.get('is_background') ## list of Tuples : [(plane_object, img_data, transparency_value), ...] existing_backgrounds = [(o, *img_info) for o in context.scene.objects \ if o.get('is_background') and (img_info := fn.get_image_infos_from_object(o)) and o.get('is_background') and o.parent] existing_backgrounds.sort(key=lambda x: x[0].parent.location.z, reverse=True) # sort by parent (holder) location Z far_plane = INIT_POS if existing_backgrounds: far_plane = existing_backgrounds[-1][0].parent.location.z ## Ensure bg_cam setup (option to use active camera ?) bg_cam = scn.objects.get('bg_cam') if not bg_cam: if not org_image_size: ## Get image size from first file img = bpy.data.images.load(file_list[0], check_existing=True) org_image_size = (img.size[0], img.size[1]) bg_cam = setup_bg_camera( org_image_size, context=context, scn=scn, cam_type=scn.camera.data.type) ## Ensure Background collection backgrounds = fn.get_col(BGCOL) # Create if needed for fp in file_list: print(f'Importing {fp.name}') current_holder = None file_stem = Path(fp).stem # current_bg = next((o for o if o.parent and fn.get_image_infos_from_object(o)), None) current_bg = next((o for o in existing_backgrounds if o[1].filepath == fp), None) if current_bg: current_holder = current_bg.parent # TODO store opacity or delete existing objects only after loop if current_bg and self.mode == 'SKIP': print(f' - SKIP: existing {current_bg.name}') continue if current_bg and self.mode == 'REPLACE': print(f' - DEL: existing {current_bg.name}') bpy.data.objects.remove(current_bg) # Import image if self.import_type == 'GPENCIL': bg_img = fn.import_image_as_gp_reference( context=context, image=fp, pack_image=False, ) elif self.import_type == 'MESH': bg_img = fn.create_image_plane(fp) elif self.import_type == 'EMPTY': bg_img = fn.create_empty_image(fp) bg_img.name = file_stem + '_texgp' bg_img.hide_select = True if current_holder: bg_img.parent = current_holder fn.set_collection(bg_img, current_holder.users_collection[0].name) continue # img, opacity = fn.get_image_infos_from_object(bg_img) print('bg_img: ', bg_img) img = fn.get_image(bg_img) # bg_name = fn.clean_image_name(img.name) file_stem = file_stem # Get create collection from image clean name bg_col = fn.get_col(file_stem, parent=backgrounds) ## Set in collection bg_col.objects.link(bg_img) print('img: ', img.name, bg_img.name, bg_img.users_collection) ## create the holder, parent to camera, set driver and set collection. Could also pass 'fp' holder = fn.create_plane_holder(img, name=PREFIX + file_stem, parent=bg_cam, link_in_col=bg_col) print('holder: ', holder.name) fn.create_plane_driver(holder, bg_cam, distance=far_plane) bg_img.parent = holder far_plane += 2 fn.reload_bg_list(scene=scn) self.report({'INFO'}, f'Settings loaded from: {os.path.basename(self.filepath)}') return {"FINISHED"} classes=( BPM_OT_import_bg_images, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)