809 lines
25 KiB
Python
809 lines
25 KiB
Python
# 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
|
||
import os
|
||
import re
|
||
import time
|
||
from pathlib import Path
|
||
from mathutils import Vector, geometry
|
||
from pprint import pprint as pp
|
||
from .constants import PREFIX, BGCOL
|
||
|
||
def get_addon_prefs():
|
||
return bpy.context.preferences.addons[__package__].preferences
|
||
|
||
def norm_str(string, separator='_', format=str.lower, padding=0):
|
||
import unicodedata
|
||
string = str(string)
|
||
string = string.replace('_', ' ')
|
||
string = string.replace('-', ' ')
|
||
string = re.sub('[ ]+', ' ', string)
|
||
string = re.sub('[ ]+\/[ ]+', '/', string)
|
||
string = string.strip()
|
||
|
||
if format:
|
||
string = format(string)
|
||
|
||
# Padd rightest number
|
||
string = re.sub(r'(\d+)(?!.*\d)', lambda x : x.group(1).zfill(padding), string)
|
||
|
||
string = string.replace(' ', separator)
|
||
string = unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode("utf-8")
|
||
|
||
return string
|
||
|
||
def clean_image_name(name):
|
||
'''Strip blender numbering then strip extension'''
|
||
name = re.sub(r'\.\d{3}$', '', name.strip())
|
||
return os.path.splitext(name)[0].strip()
|
||
|
||
def get_image_infos_from_object(ob):
|
||
'''Get image filepath of a background whatever the type
|
||
return image_data, transparency'''
|
||
|
||
transparency = None
|
||
if ob.type == 'EMPTY':
|
||
image_data = ob.data
|
||
transparency = ob.color[3]
|
||
|
||
elif ob.type == 'MESH':
|
||
texture_plane_ng = bpy.data.node_groups.get('texture_plane')
|
||
if not texture_plane_ng:
|
||
print(f'{ob.name} : No texture plane nodegroup found !')
|
||
return
|
||
|
||
mat = ob.active_material
|
||
node = next((n for n in mat.node_tree.nodes
|
||
if n.type=='GROUP' and n.node_tree is texture_plane_ng), None)
|
||
if not node:
|
||
print(f'{ob.name} : No texture plane nodegroup in material {mat.name}')
|
||
return
|
||
|
||
## Get input texture
|
||
tex_node = next(n for n in mat.node_tree.nodes
|
||
if n.type=='TEX_IMAGE')
|
||
|
||
if not tex_node:
|
||
print(f'{ob.name} : No image texture node in material {mat.name}')
|
||
return
|
||
|
||
image_data = tex_node.image
|
||
transparency = node.inputs['Transparency'].default_value
|
||
|
||
elif ob.type == 'GPENCIL':
|
||
image_data = ob.data.materials[0].grease_pencil.fill_image
|
||
transparency = ob.data.layers[0].opacity
|
||
|
||
if not image_data:
|
||
return
|
||
|
||
return (image_data, transparency)
|
||
|
||
def get_image(ob):
|
||
'''Get image data from image object'''
|
||
infos = get_image_infos_from_object(ob)
|
||
if infos is None:
|
||
return
|
||
img, _opacity = infos
|
||
return img
|
||
|
||
def get_opacity(ob):
|
||
'''Get opacity from image object'''
|
||
infos = get_image_infos_from_object(ob)
|
||
if infos is None:
|
||
return
|
||
_img, opacity = infos
|
||
return opacity
|
||
|
||
def set_opacity(ob, opacity):
|
||
'''Set opacity of BG image object whatever the type'''
|
||
if ob.type == 'EMPTY':
|
||
ob.color[3] = opacity
|
||
|
||
elif ob.type == 'MESH':
|
||
texture_plane_ng = bpy.data.node_groups.get('texture_plane')
|
||
if texture_plane_ng:
|
||
node = next(n for n in ob.active_material.node_tree.nodes
|
||
if n.type == 'GROUP' and n.node_tree is texture_plane_ng)
|
||
node.inputs['Transparency'].default_value = opacity
|
||
|
||
elif ob.type == 'GPENCIL':
|
||
ob.data.layers[0].opacity = opacity
|
||
|
||
def create_plane_holder(img, name=None, parent=None, link_in_col=None):
|
||
'''
|
||
img :: blender image or path to an image
|
||
prefix :: name holder with image name with given prefix
|
||
parent :: object to parent to (only if passed)
|
||
link_in_col :: collection to link after creation
|
||
|
||
Return created object mesh without face
|
||
None if creation failed
|
||
'''
|
||
## get image if a path is passed in
|
||
if not isinstance(img, bpy.types.Image):
|
||
try:
|
||
# load from path (str)
|
||
img = bpy.data.images.load(str(img), check_existing=True)
|
||
except:
|
||
return
|
||
|
||
# name = img.name.split('.')[0]
|
||
name = name or clean_image_name(img.name)
|
||
obj_name = name # PREFIX + name # <- always set prefix ?
|
||
x,y = img.size[:]
|
||
|
||
# create and place object plane
|
||
# find height from width (as 1)
|
||
|
||
if y < x:
|
||
h = y/x
|
||
w = 1
|
||
else:
|
||
h = 1
|
||
w = x/y
|
||
# size/2 to get center image (half width | height)
|
||
h = h/2
|
||
w = w/2
|
||
|
||
verts = [
|
||
(-w, h, 0),#2
|
||
(w, h, 0),#3
|
||
(w, -h, 0),#4
|
||
(-w, -h, 0),#1
|
||
]
|
||
|
||
mesh = bpy.data.meshes.new(obj_name)
|
||
mesh.from_pydata(verts, [(3,2), (2,1), (1,0), (0,3)], [])
|
||
holder = bpy.data.objects.new(obj_name, mesh)
|
||
holder.lock_location = (True, True, False)
|
||
holder.hide_render = True
|
||
|
||
if link_in_col is not None:
|
||
link_in_col.objects.link(holder)
|
||
# bpy.context.scene.collection.objects.link(holder)
|
||
|
||
if parent:
|
||
holder.parent = parent
|
||
return holder
|
||
|
||
## Remove func is no intermediate collection created
|
||
def get_col(name, parent=None, create=True):
|
||
parent = parent or bpy.context.scene.collection
|
||
col = bpy.data.collections.get(name)
|
||
if not col and create:
|
||
col = bpy.data.collections.new(name)
|
||
|
||
if col not in parent.children[:]:
|
||
parent.children.link(col)
|
||
|
||
return col
|
||
|
||
def create_cam(name, type='ORTHO', size=6, focale=50):
|
||
'''Create an orthographic camera with given scale
|
||
size :: othographic scale of the camera
|
||
Return camera object
|
||
'''
|
||
|
||
cam_data = bpy.data.cameras.new(name)
|
||
cam_data.type = type
|
||
cam_data.clip_start = 0.02
|
||
cam_data.clip_end = 2000
|
||
cam_data.ortho_scale = size
|
||
cam_data.lens = focale
|
||
cam = bpy.data.objects.new(name, cam_data)
|
||
cam.rotation_euler.x = 1.5707963705062866
|
||
cam.location = (0, -8, 0)
|
||
bpy.context.scene.collection.objects.link(cam)
|
||
return cam
|
||
|
||
def link_nodegroup(filepath, group_name, link=True, keep_existing=False):
|
||
ng = bpy.data.node_groups.get(group_name)
|
||
if ng:
|
||
if keep_existing:
|
||
return ng
|
||
## risk deleting nodegroup from other object !
|
||
# else:
|
||
# bpy.data.node_groups.remove(ng)
|
||
|
||
lib_path = bpy.path.relpath(str(filepath))
|
||
with bpy.data.libraries.load(lib_path, link=link) as (data_from, data_to):
|
||
data_to.node_groups = [n for n in data_from.node_groups if n.startswith(group_name)]
|
||
|
||
group = bpy.data.node_groups.get(group_name)
|
||
if group:
|
||
return group
|
||
|
||
def create_plane_driver(plane, cam, distance=None):
|
||
|
||
# Multiple planes spacing
|
||
plane.lock_location = (True, True, False)
|
||
plane.lock_rotation = (True,)*3
|
||
plane.lock_scale = (True,)*3
|
||
|
||
plane['scale'] = 1.0
|
||
plane['distance'] = distance if distance is not None else 9.0
|
||
|
||
# DRIVERS
|
||
## LOC X AND Y (shift) ##
|
||
for axis in range(2):
|
||
driver = plane.driver_add('scale', axis)
|
||
|
||
# Driver type
|
||
driver.driver.type = 'SCRIPTED'
|
||
|
||
# Variable DISTANCE
|
||
var = driver.driver.variables.new()
|
||
var.name = "distance"
|
||
#-# use a propertie to drive distance to camera
|
||
var.type = 'SINGLE_PROP'
|
||
var.targets[0].id = plane
|
||
var.targets[0].data_path = '["distance"]'
|
||
#-# Can use directly object location (but give refresh problem)
|
||
# var.type = 'TRANSFORMS'
|
||
# var.targets[0].id = plane
|
||
# var.targets[0].transform_type = 'LOC_Z'
|
||
# var.targets[0].transform_space = 'LOCAL_SPACE'
|
||
|
||
# Variable FOV
|
||
var = driver.driver.variables.new()
|
||
var.name = "FOV"
|
||
var.type = 'SINGLE_PROP'
|
||
var.targets[0].id_type = "OBJECT"
|
||
var.targets[0].id = cam
|
||
var.targets[0].data_path = 'data.angle'
|
||
|
||
# Variable scale
|
||
var = driver.driver.variables.new()
|
||
var.name = "scale"
|
||
var.type = 'SINGLE_PROP'
|
||
var.targets[0].id = plane
|
||
var.targets[0].data_path = '["scale"]'
|
||
|
||
# Expression
|
||
# -distance*tan(FOV/2)
|
||
driver.driver.expression = \
|
||
"tan(FOV/2) * distance * scale * 2"
|
||
# "tan(FOV/2) * -distance * scale * 2" # inverse dist if based on locZ
|
||
|
||
# Driver for location
|
||
dist_drv = plane.driver_add('location', 2)
|
||
dist_drv.driver.type = 'SCRIPTED' #'AVERAGE' #need to be inverted
|
||
var = dist_drv.driver.variables.new()
|
||
var.name = "distance"
|
||
var.type = 'SINGLE_PROP'
|
||
var.targets[0].id = plane
|
||
var.targets[0].data_path = '["distance"]'
|
||
dist_drv.driver.expression = '-distance'
|
||
|
||
return driver, dist_drv
|
||
|
||
def set_collection(ob, collection, unlink=True) :
|
||
''' link an object in a collection and create it if necessary
|
||
if unlink object is removed from other collections
|
||
|
||
return collection (get/created)
|
||
'''
|
||
|
||
scn = bpy.context.scene
|
||
col = None
|
||
visible = False
|
||
linked = False
|
||
|
||
# check if collection exist or create it
|
||
for c in bpy.data.collections :
|
||
if c.name == collection : col = c
|
||
if not col :
|
||
col = bpy.data.collections.new(name=collection)
|
||
|
||
# link the collection to the scene's collection if necessary
|
||
# for c in scn.collection.children :
|
||
# if c.name == col.name : visible = True
|
||
# if not visible : scn.collection.children.link(col)
|
||
|
||
# check if the object is already in the collection and link it if necessary
|
||
for o in col.objects :
|
||
if o == ob : linked = True
|
||
if not linked : col.objects.link(ob)
|
||
|
||
# remove object from scene's collection
|
||
for o in scn.collection.objects :
|
||
if o == ob : scn.collection.objects.unlink(ob)
|
||
|
||
# if unlink flag we remove the object from other collections
|
||
if unlink :
|
||
for c in ob.users_collection :
|
||
if c.name != collection : c.objects.unlink(ob)
|
||
|
||
return col
|
||
|
||
def placeholder_name(name='', context=None) -> str:
|
||
|
||
# def increment(match):
|
||
# return str(int(match.group(1))+1).zfill(len(match.group(1)))
|
||
|
||
context = context or bpy.context
|
||
name = name.strip()
|
||
if not name:
|
||
# Create a default name (fing last 3 number, before blender increment if exists, default increment)
|
||
numbers = [int(match.group(1)) for o in context.scene.objects\
|
||
if (match := re.search(r'^drawing.*_(\d+)(?:\.\d{3})?$', o.name, flags=re.I))\
|
||
# and o.type == 'GPENCIL'
|
||
]
|
||
if numbers:
|
||
numbers.sort()
|
||
name = f'drawing_{numbers[-1] + 1:03d}'
|
||
else:
|
||
name = 'drawing_001'
|
||
|
||
# elif context.scene.objects.get(name):
|
||
# name = re.sub(r'(\d+)(?!.*\d)', increment, name) # find rightmost number
|
||
|
||
return name
|
||
|
||
def reset_gp_uv(ob):
|
||
uvs = [(0.5, 0.5), (-0.5, 0.5), (-0.5, -0.5), (0.5, -0.5)]
|
||
try:
|
||
for p, uv in zip(ob.data.layers[0].frames[0].strokes[0].points, uvs):
|
||
p.uv_fill = uv
|
||
except:
|
||
print('Could not set Gp points UV')
|
||
|
||
## Get empty coords
|
||
def get_ref_object_space_coord(o):
|
||
size = o.empty_display_size
|
||
x,y = o.empty_image_offset
|
||
img = o.data
|
||
|
||
res_x, res_y = img.size
|
||
scaling = 1 / max(res_x, res_y)
|
||
|
||
# 3----2
|
||
# | |
|
||
# 0----1
|
||
|
||
corners = [
|
||
Vector((0,0)),
|
||
Vector((res_x, 0)),
|
||
Vector((0, res_y)),
|
||
Vector((res_x, res_y)),
|
||
]
|
||
|
||
obj_space_corners = []
|
||
for co in corners:
|
||
nco_x = ((co.x + (x * res_x)) * size) * scaling
|
||
nco_y = ((co.y + (y * res_y)) * size) * scaling
|
||
obj_space_corners.append(Vector((nco_x, nco_y, 0)))
|
||
return obj_space_corners
|
||
|
||
|
||
## Generate GP object
|
||
def create_gpencil_reference(
|
||
gpd: bpy.types.GreasePencil,
|
||
gpf: bpy.types.GPencilFrame,
|
||
image: bpy.types.Image,
|
||
) -> bpy.types.GPencilStroke:
|
||
"""
|
||
Add a rectangular stroke textured with `image` to the given grease pencil fame.
|
||
:param gpd: The grease pencil data.
|
||
:param gpf: The grease pencil frame.
|
||
:param image: The image to use as texture.
|
||
:return: The created grease pencil stroke.
|
||
"""
|
||
name = clean_image_name(image.name) + '_texgp'
|
||
|
||
# Create new material
|
||
mat = bpy.data.materials.new(f".ref/{name}")
|
||
bpy.data.materials.create_gpencil_data(mat)
|
||
gpd.materials.append(mat)
|
||
idx = gpd.materials.find(mat.name)
|
||
|
||
# Setup material settings
|
||
mat.grease_pencil.show_stroke = False
|
||
mat.grease_pencil.show_fill = True
|
||
mat.grease_pencil.fill_image = image
|
||
mat.grease_pencil.fill_style = "TEXTURE"
|
||
mat.grease_pencil.mix_factor = 0.0
|
||
mat.grease_pencil.texture_offset = (0.0, 0.0)
|
||
mat.grease_pencil.texture_angle = 0.0
|
||
mat.grease_pencil.texture_scale = (1.0, 1.0)
|
||
mat.grease_pencil.texture_clamp = True
|
||
|
||
# Create the stroke
|
||
gps_new = gpf.strokes.new()
|
||
gps_new.points.add(4, pressure=0, strength=0)
|
||
|
||
## This will make sure that the uv's always remain the same
|
||
# gps_new.use_automatic_uvs = False # <<- /!\ exists only in SPA core changes
|
||
|
||
gps_new.use_cyclic = True
|
||
gps_new.material_index = idx
|
||
|
||
x,y = image.size[:]
|
||
|
||
# create and place object plane
|
||
# find height from width (as 1)
|
||
if y < x:
|
||
h = y/x
|
||
w = 1
|
||
else:
|
||
h = 1
|
||
w = x/y
|
||
# size/2 to get center image (half width | height)
|
||
h = h/2
|
||
w = w/2
|
||
|
||
coords = [
|
||
(w, h, 0),
|
||
(-w, h, 0),
|
||
(-w, -h, 0),
|
||
(w, -h, 0),
|
||
]
|
||
|
||
uvs = [(0.5, 0.5), (-0.5, 0.5), (-0.5, -0.5), (0.5, -0.5)]
|
||
|
||
for i, (co, uv) in enumerate(zip(coords, uvs)):
|
||
gps_new.points[i].co = co
|
||
gps_new.points[i].uv_fill = uv
|
||
|
||
return gps_new
|
||
|
||
def import_image_as_gp_reference(
|
||
image,
|
||
pack_image: bool = False,
|
||
):
|
||
"""
|
||
Import image from `image` as a textured rectangle in the given
|
||
grease pencil object.
|
||
:param image: The image filepath or image datablock
|
||
:param pack_image: Whether to pack the image into the Blender file.
|
||
"""
|
||
|
||
if not image:
|
||
return
|
||
|
||
scene = bpy.context.scene
|
||
if not isinstance(image, bpy.types.Image):
|
||
image = bpy.data.images.load(str(image), check_existing=True)
|
||
|
||
## Create grease pencil object
|
||
gpd = bpy.data.grease_pencils.new(image.name)
|
||
ob = bpy.data.objects.new(image.name, gpd)
|
||
gpl = gpd.layers.new(image.name)
|
||
gpf = gpl.frames.new(0) # TDOO: test negative
|
||
|
||
if pack_image:
|
||
image.pack()
|
||
|
||
gps: bpy.types.GPencilStroke = create_gpencil_reference(
|
||
gpd,
|
||
gpf,
|
||
image,
|
||
)
|
||
|
||
gps.select = True # Not needed
|
||
|
||
cam = scene.camera # should be bg_cam
|
||
|
||
# Create the driver and parent
|
||
ob.use_grease_pencil_lights = False
|
||
ob['is_background'] = True
|
||
print(f'Created {ob.name} GP background object')
|
||
return ob
|
||
|
||
def gp_transfer_mode(ob, context=None):
|
||
context= context or bpy.context
|
||
if ob.type != 'GPENCIL' or context.object is ob:
|
||
return
|
||
|
||
prev_mode = context.mode
|
||
possible_gp_mods = ('OBJECT',
|
||
'EDIT_GPENCIL', 'SCULPT_GPENCIL', 'PAINT_GPENCIL',
|
||
'WEIGHT_GPENCIL', 'VERTEX_GPENCIL')
|
||
|
||
if prev_mode not in possible_gp_mods:
|
||
prev_mode = None
|
||
|
||
mode_swap = False
|
||
|
||
## TODO optional: Option to stop mode sync ?
|
||
## Set in same mode as previous object
|
||
if context.scene.tool_settings.lock_object_mode:
|
||
if context.mode != 'OBJECT':
|
||
mode_swap = True
|
||
bpy.ops.object.mode_set(mode='OBJECT')
|
||
|
||
# set active
|
||
context.view_layer.objects.active = ob
|
||
|
||
## keep same mode accross objects
|
||
if mode_swap and prev_mode is not None:
|
||
bpy.ops.object.mode_set(mode=prev_mode)
|
||
|
||
else:
|
||
## keep same mode accross objects
|
||
context.view_layer.objects.active = ob
|
||
if context.mode != prev_mode is not None:
|
||
bpy.ops.object.mode_set(mode=prev_mode)
|
||
|
||
for o in [o for o in context.scene.objects if o.type == 'GPENCIL']:
|
||
o.select_set(o == ob) # select only active (when not in object mode)
|
||
|
||
## generate tex plane
|
||
# def create_image_plane(coords, name):
|
||
# '''Create an a mesh plane with a defaut UVmap from passed coordinate
|
||
# object and mesh get passed name
|
||
|
||
# return plane object
|
||
# '''
|
||
# fac = [(0, 1, 3, 2)]
|
||
# me = bpy.data.meshes.new(name)
|
||
# me.from_pydata(coords, [], fac)
|
||
# plane = bpy.data.objects.new(name, me)
|
||
# # col = bpy.context.collection
|
||
# # col.objects.link(plane)
|
||
|
||
# me.uv_layers.new(name='UVMap')
|
||
# return plane
|
||
|
||
def create_material(name, img, node_group, keep_existing=False):
|
||
if keep_existing:
|
||
## Reuse if exist and seem clean
|
||
m = bpy.data.materials.get(name)
|
||
if m:
|
||
if name in m.name and m.use_nodes:
|
||
valid_image = next((n for n in m.node_tree.nodes if n.type == 'TEX_IMAGE' and n.image == img), None)
|
||
valid_nodegroup = next((n for n in m.node_tree.nodes if n.type == 'GROUP' and n.node_tree), None)
|
||
if valid_image and valid_nodegroup:
|
||
# Replace node_tree if different
|
||
print('node_group: ', node_group)
|
||
print('valid_nodegroup.node_tree: ', valid_nodegroup.node_tree)
|
||
if node_group != valid_nodegroup.node_tree:
|
||
valid_nodegroup = node_group
|
||
return m
|
||
|
||
# create mat
|
||
mat = bpy.data.materials.new(name)
|
||
mat.blend_method = 'BLEND' # 'CLIP' (clip is able to respect space)
|
||
|
||
mat.use_nodes = True
|
||
|
||
node_tree = mat.node_tree
|
||
nodes = node_tree.nodes
|
||
links = node_tree.links
|
||
|
||
# clear default BSDG
|
||
principled = nodes.get('Principled BSDF')
|
||
if principled: nodes.remove(principled)
|
||
mat_output = nodes.get('Material Output')
|
||
|
||
group = nodes.new("ShaderNodeGroup")
|
||
group.node_tree = node_group
|
||
group.location = (0, 276)
|
||
|
||
img_tex = nodes.new('ShaderNodeTexImage')
|
||
img_tex.image = img
|
||
img_tex.interpolation = 'Linear' # 'Closest','Cubic','Smart'
|
||
img_tex.extension = 'CLIP' # or EXTEND
|
||
img_tex.location = (-400, 218)
|
||
|
||
# links.new(img_tex.outputs['Color'], group.inputs['Color'])
|
||
# links.new(img_tex.outputs['Alpha'], group.inputs['Alpha'])
|
||
links.new(img_tex.outputs[0], group.inputs[0])
|
||
links.new(img_tex.outputs[1], group.inputs[1])
|
||
links.new(group.outputs[0], mat_output.inputs[0])
|
||
|
||
return mat
|
||
|
||
|
||
def create_image_plane(img, node_group, parent=None):
|
||
'''
|
||
img :: blender image or path to an image
|
||
parent :: object to parent to (only if passed)
|
||
|
||
Return created object or None if creation failed
|
||
'''
|
||
|
||
## get image if a path is passed in
|
||
if not isinstance(img, bpy.types.Image):
|
||
try:
|
||
img = bpy.data.images.load(str(img), check_existing=True)
|
||
except:
|
||
return
|
||
|
||
name = img.name.split('.')[0]
|
||
obj_name = f'{name}_texplane'
|
||
x,y = img.size[:]
|
||
|
||
# create and place object plane
|
||
# find height from width (as 1)
|
||
if y < x:
|
||
h = y/x
|
||
w = 1
|
||
else:
|
||
h = 1
|
||
w = x/y
|
||
# size/2 to get center image (half width | height)
|
||
h = h/2
|
||
w = w/2
|
||
|
||
verts = [
|
||
(-w, h, 0),#2
|
||
(w, h, 0),#3
|
||
(w, -h, 0),#4
|
||
(-w, -h, 0),#1
|
||
]
|
||
|
||
faces = [(3, 2, 1, 0)]
|
||
mesh = bpy.data.meshes.new(obj_name)
|
||
mesh.from_pydata(verts, [], faces)
|
||
plane = bpy.data.objects.new(obj_name, mesh)
|
||
|
||
# setup material (link nodegroup)
|
||
|
||
plane.data.uv_layers.new()
|
||
|
||
# Create and assign material
|
||
|
||
mat = create_material(name, img, node_group)
|
||
plane.data.materials.append(mat)
|
||
bpy.context.scene.collection.objects.link(plane)
|
||
# full lock (or just kill selectability...)
|
||
plane.lock_location = [True]*3
|
||
plane.hide_select = True
|
||
|
||
if parent:
|
||
plane.parent = parent
|
||
|
||
return plane
|
||
|
||
def create_empty_image(image):
|
||
if not isinstance(image, bpy.types.Image):
|
||
try:
|
||
image = bpy.data.images.load(str(image), check_existing=True)
|
||
except:
|
||
return
|
||
|
||
# ob = bpy.data.objects.new(f'{image_name}_texempty', None)
|
||
ob = bpy.data.objects.new(image.name, None)
|
||
# ob.hide_viewport = (cam_type == 'ORTHO') # hide if in persp mode
|
||
|
||
# load image on the empty
|
||
ob.empty_display_type = 'IMAGE'
|
||
ob.data = image
|
||
# ob.empty_display_size = bg_cam.data.ortho_scale # set display size
|
||
ob.use_empty_image_alpha = True
|
||
ob.color[3] = 0.5 # transparency
|
||
return ob
|
||
|
||
## Scan background objects and reload
|
||
|
||
def scan_backgrounds(context=None, scene=None, all_bg=False):
|
||
'''return a list (sorted by negative locZ)
|
||
of all holder object (parent of texempty and texplane)
|
||
'''
|
||
|
||
context = context or bpy.context
|
||
scene = scene or context.scene
|
||
|
||
oblist = []
|
||
if all_bg:
|
||
# oblist = [o for o in scene.objects if o.get('is_background_holder')]
|
||
oblist = [o for o in scene.objects if o.name.startswith(PREFIX) and o.type == 'MESH']
|
||
else:
|
||
col = bpy.data.collections.get(BGCOL)
|
||
if not col:
|
||
return oblist
|
||
# oblist = [o for o in col.all_objects if o.get('is_background_holder')]
|
||
oblist = [o for o in col.all_objects if o.name.startswith(PREFIX) and o.type == 'MESH']
|
||
|
||
oblist.sort(key=lambda x : x.location.z, reverse=True)
|
||
return oblist
|
||
|
||
def add_loc_z_driver(plane):
|
||
# Create properties
|
||
# plane['distance'] = abs(plane.location.z)
|
||
plane['distance'] = -plane.location.z
|
||
# Driver for location
|
||
dist_drv = plane.driver_add('location', 2)
|
||
dist_drv.driver.type = 'SCRIPTED' #'AVERAGE' #need to be inverted
|
||
var = dist_drv.driver.variables.new()
|
||
var.name = "distance"
|
||
var.type = 'SINGLE_PROP'
|
||
var.targets[0].id = plane
|
||
var.targets[0].data_path = '["distance"]'
|
||
dist_drv.driver.expression = '-distance'
|
||
|
||
def reload_bg_list(scene=None):
|
||
'''List holder plane object and add them to bg collection'''
|
||
start = time.time()
|
||
scn = scene or bpy.context.scene
|
||
uilist = scn.bg_props.planes
|
||
# current_index = scn.bg_props.index
|
||
|
||
oblist = scan_backgrounds()
|
||
# print('reload_bg_list > oblist:')
|
||
# pp(oblist)
|
||
warning = []
|
||
if not oblist:
|
||
return f'No BG found, need to be in collection "{BGCOL}" !'
|
||
|
||
## Clean
|
||
for ob in oblist:
|
||
bg_viewlayer = bpy.context.view_layer.layer_collection.children.get(BGCOL)
|
||
if not bg_viewlayer:
|
||
return "No 'Background' collection in scene master collection !"
|
||
|
||
ob_viewcol = bg_viewlayer.children.get(ob.name)
|
||
if not ob_viewcol:
|
||
warning.append(f'{ob.name} not found in "Background" collection')
|
||
continue
|
||
|
||
# if vl_col is hided or object is hided
|
||
# visible = any(o for o in ob.children if o.visible_get()) # ob.visible_get()
|
||
# for o in ob.children: o.hide_set(False)
|
||
|
||
ob_viewcol.hide_viewport = False
|
||
ob_viewcol.collection.hide_viewport = False
|
||
# ob_viewcol.exclude = not visible
|
||
|
||
## Driver check (don't erase if exists, else will delete persp mode drivers...)
|
||
if not ob.get('distance'):
|
||
if ob.animation_data:
|
||
for dr in ob.animation_data.drivers:
|
||
if dr.driver.variables.get('distance'):
|
||
# dist = ob.get('distance')
|
||
ob.animation_data.drivers.remove(dr)
|
||
add_loc_z_driver(ob)
|
||
|
||
uilist.clear()
|
||
|
||
## Populate the list
|
||
for ob in oblist: # populate list
|
||
item = uilist.add()
|
||
item.plane = ob
|
||
item.type = "bg"
|
||
scn.bg_props.index = len(uilist) - 1 # id (len of list in the ad loop)
|
||
if ob.children and ob.children[0].type == 'GPENCIL':
|
||
reset_gp_uv(ob.children[0]) # Force reset UV
|
||
|
||
## Add Grease pencil objects
|
||
gp_list = [o for o in bpy.context.scene.objects if o.type == 'GPENCIL' \
|
||
and not o.get('is_background') and o not in oblist]
|
||
for ob in gp_list:
|
||
item = uilist.add()
|
||
item.plane = ob
|
||
item.type = "obj"
|
||
scn.bg_props.index = len(uilist) - 1 # id (len of list in the ad loop)
|
||
|
||
print(f"{len(uilist)} loaded in {time.time()-start:.2f}")
|
||
return warning
|
||
|
||
|
||
def coord_distance_from_cam_straight(coord=None, context=None, camera=None):
|
||
context = context or bpy.context
|
||
coord = coord or context.scene.cursor.location
|
||
camera = camera or context.scene.camera
|
||
if not camera:
|
||
return
|
||
|
||
view_mat = camera.matrix_world
|
||
view_point = view_mat @ Vector((0, 0, -1000))
|
||
co = geometry.intersect_line_plane(view_mat.translation, view_point, coord, view_point)
|
||
if co is None:
|
||
return None
|
||
return (co - view_mat.translation).length
|
||
|
||
def coord_distance_from_cam(coord, context=None, camera=None):
|
||
context = context or bpy.context
|
||
coord = coord or context.scene.cursor.location
|
||
camera = camera or context.scene.camera
|
||
if not camera:
|
||
return
|
||
return (coord - camera.matrix_world.to_translation()).length
|
||
|