407 lines
14 KiB
Python
407 lines
14 KiB
Python
import re
|
|
from math import pi
|
|
|
|
import bpy
|
|
import mathutils
|
|
from bpy.types import Operator
|
|
from mathutils import Matrix, Vector
|
|
|
|
from .. import core
|
|
|
|
|
|
def set_resolution_from_cam_prop(cam=None):
|
|
if not cam:
|
|
cam = bpy.context.scene.camera
|
|
if not cam:
|
|
return ('ERROR', 'No active camera')
|
|
|
|
res = cam.get('resolution')
|
|
if not res:
|
|
return ('ERROR', 'Cam has no resolution attribute')
|
|
|
|
rd = bpy.context.scene.render
|
|
if rd.resolution_x == res[0] and rd.resolution_y == res[1]:
|
|
return ('INFO', f'Resolution already at {res[0]}x{res[1]}')
|
|
else:
|
|
rd.resolution_x, rd.resolution_y = res[0], res[1]
|
|
return ('INFO', f'Resolution to {res[0]}x{res[1]}')
|
|
|
|
|
|
class BPM_OT_swap_cams(Operator):
|
|
bl_idname = "bpm.swap_cams"
|
|
bl_label = "Swap Cameras"
|
|
bl_description = "Toggle between anim and bg cam"
|
|
bl_options = {"REGISTER"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return True
|
|
|
|
def execute(self, context):
|
|
rd = bpy.context.scene.render
|
|
|
|
anim_cam = bpy.context.scene.objects.get('anim_cam')
|
|
bg_cam = bpy.context.scene.objects.get('bg_cam')
|
|
|
|
if not anim_cam or not bg_cam:
|
|
self.report({'ERROR'}, 'anim_cam or bg_cam is missing')
|
|
return {"CANCELLED"}
|
|
|
|
if anim_cam:
|
|
res = anim_cam.get('resolution')
|
|
if not res:
|
|
anim_cam['resolution'] = [rd.resolution_x, rd.resolution_y]
|
|
|
|
cam = context.scene.camera
|
|
if not cam:
|
|
context.scene.camera = anim_cam
|
|
set_resolution_from_cam_prop()
|
|
return {"FINISHED"}
|
|
|
|
in_draw = False
|
|
if cam.parent and cam.name in ('draw_cam', 'action_cam'):
|
|
if cam.name == 'draw_cam':
|
|
draw_cam = cam
|
|
in_draw = True
|
|
cam = cam.parent
|
|
|
|
## swap
|
|
# context.scene.camera = bg_cam if cam is anim_cam else anim_cam
|
|
if cam is anim_cam:
|
|
main = context.scene.camera = bg_cam
|
|
anim_cam.hide_viewport = True
|
|
bg_cam.hide_viewport = False
|
|
else:
|
|
main = context.scene.camera = anim_cam
|
|
anim_cam.hide_viewport = False
|
|
bg_cam.hide_viewport = True
|
|
|
|
if in_draw:
|
|
draw_cam.parent = main
|
|
draw_cam.data = main.data
|
|
# back in draw_cam
|
|
context.scene.camera = draw_cam
|
|
bg_cam.hide_viewport = anim_cam.hide_viewport = True
|
|
|
|
# set res
|
|
ret = set_resolution_from_cam_prop(main)
|
|
if ret:
|
|
self.report({ret[0]}, ret[1])
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class BPM_OT_send_gp_to_plane(Operator):
|
|
bl_idname = "bpm.send_gp_to_plane"
|
|
bl_label = "Send To Plane"
|
|
bl_description = "Send the selected GPs to current active layer, adjusting scale to keep size in camera"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object and context.object.type != 'CAMERA'
|
|
|
|
def execute(self, context):
|
|
offset = 0.005 # 0.001
|
|
ob = context.object
|
|
|
|
cam = context.scene.camera
|
|
|
|
settings = context.scene.bg_props
|
|
plane = settings.planes[settings.index].plane
|
|
plane_mat = plane.matrix_world
|
|
|
|
plane_co = plane_mat.translation
|
|
plane_no = Vector((0,0,1))
|
|
plane_no.rotate(plane_mat)
|
|
|
|
cam_co = cam.matrix_world.translation
|
|
ob_co = ob.matrix_world.translation
|
|
|
|
if cam.data.type == 'ORTHO':
|
|
forward = Vector((0,0,-1))
|
|
backward = Vector((0,0,1))
|
|
forward.rotate(cam.matrix_world)
|
|
backward.rotate(cam.matrix_world)
|
|
new_co = mathutils.geometry.intersect_line_plane(ob_co, ob_co+forward, plane_co, plane_no)
|
|
if not new_co:
|
|
new_co = mathutils.geometry.intersect_line_plane(ob_co, ob_co+backward, plane_co, plane_no)
|
|
if not new_co:
|
|
self.report({'ERROR'}, 'Could not hit background surface by tracing looking in cam direction from obj\nCheck if BG plane is parallel to camera view')
|
|
return {"CANCELLED"}
|
|
|
|
new_vec = new_co - ob_co
|
|
new_vec += backward * offset
|
|
ob.matrix_world.translation += new_vec
|
|
self.report({'INFO'}, f'Moved {ob.name} to {plane.name} ({new_vec.length:.3f}m)')
|
|
return {"FINISHED"}
|
|
|
|
# PERSP mode
|
|
init_dist = (ob_co - cam_co).length
|
|
new_co = mathutils.geometry.intersect_line_plane(cam_co, ob_co, plane_co, plane_no)
|
|
print('new_co: ', new_co)
|
|
if not new_co:
|
|
self.report({'ERROR'}, 'Grease pencil object might be behind camera\nCould not hit background surface by tracing from cam to BG')
|
|
return {"CANCELLED"}
|
|
new_vec = new_co - cam_co
|
|
new_vec -= new_vec.normalized() * offset # substract offset from cam to ob vector
|
|
new_dist = new_vec.length # check distance after offset applied for right scaling
|
|
dist_percentage = new_dist / init_dist
|
|
ob.matrix_world.translation = cam_co + new_vec # replace from cam to ob
|
|
|
|
ob.scale = ob.matrix_world.to_scale() * dist_percentage # adjust scale
|
|
self.report({'INFO'}, f'Moved {ob.name} to {plane.name} ({new_dist:.3f}m)')
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
class BPM_OT_parent_to_bg(Operator):
|
|
bl_idname = "bpm.parent_to_bg"
|
|
bl_label = "Parent To Selected Background"
|
|
bl_description = "Parent selected active object to active Background in list"
|
|
bl_options = {"REGISTER"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object# and context.object.type != 'CAMERA'
|
|
|
|
def execute(self, context):
|
|
settings = context.scene.bg_props
|
|
plane = settings.planes[settings.index].plane
|
|
plane_list = [i.plane for i in settings.planes]
|
|
# plane_mat = plane.matrix_world
|
|
o = bpy.context.object
|
|
|
|
if o in plane_list:
|
|
self.report({'ERROR'}, 'Selected object must not be a plane')
|
|
return {"CANCELLED"}
|
|
|
|
mat = o.matrix_world.copy()
|
|
if o.parent:
|
|
parent = o.parent
|
|
o.parent = None
|
|
self.report({'INFO'}, f'Object "{o.name}" unparented from {parent.name}')
|
|
else:
|
|
o.parent = plane
|
|
self.report({'INFO'}, f'Object "{o.name}" parented to {plane.name}')
|
|
|
|
o.matrix_world = mat
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
# TODO make the align to plane orientation (change object rotation without affecting points)
|
|
# need to make a loop with frame_set on each frame (and change only relevant layers...)
|
|
|
|
class BPM_OT_align_to_plane(Operator):
|
|
bl_idname = "bpm.align_to_plane"
|
|
bl_label = "Align to GP plane"
|
|
bl_description = "Align the current GP object to plane\n(change object orientation while keeping points in place)"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return True
|
|
|
|
def execute(self, context):
|
|
settings = context.scene.bg_props
|
|
plane = settings.planes[settings.index].plane
|
|
plane_mat = plane.matrix_world
|
|
|
|
o = bpy.context.object
|
|
old_mat = o.matrix_world.copy()
|
|
|
|
# reset or align to needed plane
|
|
|
|
# decompose old matrix
|
|
loc, rot, scale = old_mat.decompose()
|
|
|
|
matloc = Matrix.Translation(loc)
|
|
|
|
#matrot = Matrix()
|
|
matrot = rot.to_matrix().to_4x4()
|
|
|
|
# recreate a neutral mat scale
|
|
matscale_x = Matrix.Scale(scale[0], 4,(1,0,0))
|
|
matscale_y = Matrix.Scale(scale[1], 4,(0,1,0))
|
|
matscale_z = Matrix.Scale(scale[2], 4,(0,0,1))
|
|
matscale = matscale_x @ matscale_y @ matscale_z
|
|
|
|
#C.object.rotation_euler = (0,0,0)
|
|
|
|
# mat_90 = Matrix.Rotation(-pi/2, 4, 'X')
|
|
|
|
print("old_mat", old_mat)#Dbg
|
|
#new_mat = o.matrix_world.copy()
|
|
|
|
new_mat = matloc @ matscale
|
|
context.object.matrix_world = new_mat
|
|
|
|
|
|
for l in o.data.layers:
|
|
for f in l.frames:
|
|
for s in f.strokes:
|
|
for p in s.points:
|
|
p.co = matrot @ p.co
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
def place_object_from_facing_cam(ob=None, cam=None, distance=8):
|
|
ob = ob or bpy.context.object
|
|
cam = cam or bpy.context.scene.camera
|
|
if not cam:
|
|
return
|
|
scale = ob.matrix_world.to_scale()
|
|
mat_scale_x = Matrix.Scale(scale[0], 4,(1,0,0))
|
|
mat_scale_y = Matrix.Scale(scale[1], 4,(0,1,0))
|
|
mat_scale_z = Matrix.Scale(scale[2], 4,(0,0,1))
|
|
mat_scale = mat_scale_x @ mat_scale_y @ mat_scale_z
|
|
|
|
mat = cam.matrix_world.copy()
|
|
cam_mat_inv = mat.inverted()
|
|
|
|
mat_90 = Matrix.Rotation(-pi/2, 4, 'X')
|
|
|
|
# Offset in object local Y
|
|
mat.translation -= Vector((0, 0, distance)) @ cam_mat_inv
|
|
mat = mat @ mat_90 @ mat_scale
|
|
ob.matrix_world = mat
|
|
|
|
class BPM_OT_create_and_place_in_camera(Operator):
|
|
bl_idname = "bpm.create_and_place_in_camera"
|
|
bl_label = "Create and Place Gpencil In Camera"
|
|
bl_description = "Create GP object\
|
|
\nCentered and Rotated so X-Z front axis is facing cam\
|
|
\nCtrl + Click to place selected object intead of creating"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not context.scene.camera:
|
|
cls.poll_message_set("Need a scene camera, object is created facing cam")
|
|
return False
|
|
return True
|
|
|
|
create : bpy.props.BoolProperty(name='Create', default=False, options={'SKIP_SAVE'})
|
|
name : bpy.props.StringProperty(name='Name', default='', options={'SKIP_SAVE'})
|
|
distance : bpy.props.FloatProperty(name='Distance', default=8, subtype='DISTANCE')
|
|
use_light : bpy.props.BoolProperty(name='Use Light', default=False, options={'SKIP_SAVE'})
|
|
edit_line_opacity : bpy.props.FloatProperty(name='Edit Line Opacity',
|
|
description="Edit line opacity for newly created objects\
|
|
\nAdvanced users generally like it at 0 (show only selected line in edit mode)\
|
|
\nBlender default is 0.5",
|
|
default=0.0, min=0.0, max=1.0)
|
|
|
|
def invoke(self, context, event):
|
|
if event.ctrl:
|
|
self.create = False
|
|
|
|
## Set placeholder name (Comment to let an empty string)
|
|
self.name = core.placeholder_name(self.name, context)
|
|
prefs = core.get_addon_prefs()
|
|
self.use_light = prefs.use_light
|
|
self.edit_line_opacity = prefs.edit_line_opacity
|
|
|
|
if not bpy.context.scene.objects.get('bg_cam'):
|
|
self.report({'ERROR'}, 'No bg_cam')
|
|
return {"CANCELLED"}
|
|
|
|
## match current plane distance
|
|
settings = context.scene.bg_props
|
|
|
|
if settings.planes and settings.planes[settings.index].plane:
|
|
plane = settings.planes[settings.index].plane
|
|
self.distance = core.coord_distance_from_cam_straight(plane.matrix_world.to_translation()) - 0.005
|
|
else:
|
|
self.distance = core.coord_distance_from_cam_straight(context.scene.cursor.location)
|
|
self.distance = max([1.0, self.distance]) # minimum one meter away from cam
|
|
|
|
if self.create:
|
|
return context.window_manager.invoke_props_dialog(self, width=250)
|
|
else:
|
|
if not context.object:
|
|
self.report({'ERROR'}, 'No active object')
|
|
return {"CANCELLED"}
|
|
|
|
return self.execute(context)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.prop(self, 'name', icon='OUTLINER_OB_GREASEPENCIL')
|
|
layout.prop(self, 'distance', icon='DRIVER_DISTANCE')
|
|
layout.separator()
|
|
layout.prop(self, 'use_light')
|
|
layout.prop(self, 'edit_line_opacity')
|
|
|
|
def execute(self, context):
|
|
if self.create:
|
|
ob_name = core.placeholder_name(self.name, context)
|
|
|
|
## Create Object
|
|
prefs = core.get_addon_prefs()
|
|
gp_data = bpy.data.grease_pencils.new(ob_name)
|
|
ob = bpy.data.objects.new(ob_name, gp_data)
|
|
ob.use_grease_pencil_lights = prefs.use_light
|
|
gp_data.edit_line_color[3] = prefs.edit_line_opacity
|
|
l = gp_data.layers.new('GP_Layer')
|
|
l.frames.new(context.scene.frame_current)
|
|
|
|
gp_col = core.set_collection(ob, 'GP') # Gpencils
|
|
## Add Collection to current scene
|
|
if not gp_col in context.scene.collection.children_recursive:
|
|
context.scene.collection.children.link(gp_col)
|
|
|
|
# Add to bg_plane collection
|
|
new_item = context.scene.bg_props.planes.add()
|
|
new_item.plane = ob
|
|
new_item.type = 'obj'
|
|
|
|
# Set active on last
|
|
context.scene.bg_props.index = len(context.scene.bg_props.planes) - 1
|
|
core.gp_transfer_mode(ob)
|
|
|
|
loaded_palette = False
|
|
if hasattr(bpy.types, "GPTB_OT_load_default_palette"):
|
|
res = bpy.ops.gp.load_default_palette()
|
|
if res == {"FINISHED"}:
|
|
loaded_palette = True
|
|
|
|
if not loaded_palette:
|
|
# Append at least line material
|
|
mat = bpy.data.materials.get('line')
|
|
if not mat:
|
|
## Create basic GP mat
|
|
mat = bpy.data.materials.new(name='line')
|
|
bpy.data.materials.create_gpencil_data(mat)
|
|
gp_data.materials.append(mat)
|
|
|
|
else:
|
|
ob = context.object
|
|
|
|
## Place in centered and front facing camera at given distance
|
|
cam = context.scene.camera
|
|
place_object_from_facing_cam(ob, cam, self.distance)
|
|
return {"FINISHED"}
|
|
|
|
|
|
classes=(
|
|
## Scene
|
|
BPM_OT_swap_cams,
|
|
|
|
## GP related
|
|
BPM_OT_send_gp_to_plane,
|
|
BPM_OT_parent_to_bg,
|
|
BPM_OT_create_and_place_in_camera,
|
|
# BPM_OT_align_to_plane # << TODO
|
|
)
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
|
|
def unregister():
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|