From 07a44190c9c1123e93550d2ffd54d4000d230653 Mon Sep 17 00:00:00 2001 From: Pullusb Date: Tue, 4 May 2021 18:18:42 +0200 Subject: [PATCH] initial setup for GP realign --- OP_realign.py | 276 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 OP_realign.py diff --git a/OP_realign.py b/OP_realign.py new file mode 100644 index 0000000..1fb68c0 --- /dev/null +++ b/OP_realign.py @@ -0,0 +1,276 @@ +import bpy +import mathutils +from mathutils import Matrix, Vector +from math import pi +import numpy as np +from time import time + +def batch_reproject(obj, project_type='VIEW', all_strokes=True, restore_frame=False): + '''Reproject - ops method + :all_stroke: affect hided, locked layers + ''' + + if restore_frame: + oframe = bpy.context.scene.frame_current + omode = bpy.context.mode + + # FIXME : if all_stroke is False, might be better to still store>set>restore "lock_frame" + if all_strokes: + layers_state = [[l, l.hide, l.lock, l.lock_frame] for l in obj.data.layers] + for l in obj.data.layers: + l.hide = False + l.lock = False + l.lock_frame = False + + bpy.ops.object.mode_set(mode='EDIT_GPENCIL') + # TODO : need to toggle visible and unlock all layers before attacking, and store restore. + + for l in obj.data.layers: + for f in l.frames: + bpy.context.scene.frame_set(f.frame_number) + # switch to edit to reproject through ops + bpy.ops.gpencil.select_all(action='SELECT') + bpy.ops.gpencil.reproject(type=project_type) # default is VIEW + bpy.ops.gpencil.select_all(action='DESELECT') + + # restore + if all_strokes: + for layer, hide, lock, lock_frame in layers_state: + layer.hide = hide + layer.lock = lock + layer.lock_frame = lock_frame + + bpy.ops.object.mode_set(mode=omode) + + if restore_frame: + bpy.context.scene.frame_current = oframe + + +def align_global(reproject=True, ref=None): + + if not ref: + ref = bpy.context.scene.camera + + o = bpy.context.object + # if o.matrix_basis != o.matrix_world and not o.parent: + + ref = bpy.context.scene.camera + ref_mat = ref.matrix_world + ref_loc, ref_rot, ref_scale = ref_mat.decompose() + + if o.parent: + mat = o.matrix_world + else: + mat = o.matrix_basis + + o_loc, o_rot, o_scale = mat.decompose() + + mat_90 = Matrix.Rotation(-pi/2, 4, 'X') + + loc_mat = Matrix.Translation(o_loc) + rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90 + scale_mat = get_scale_matrix(o_scale) + + new_mat = loc_mat @ rot_mat @ scale_mat + + # world_coords = [] + for l in o.data.layers: + for f in l.frames: + for s in f.strokes: + ## foreach + coords = [p.co @ mat.inverted() @ new_mat for p in s.points] + # print('coords: ', coords) + # print([co for v in coords for co in v]) + s.points.foreach_set('co', [co for v in coords for co in v]) + # s.points.update() # seem to works # but adding/deleting a point is "safer" + ## force update + s.points.add(1) + s.points.pop() + + # for p in s.points: + ## GOOD : + # world_co = mat @ p.co + # p.co = new_mat.inverted() @ world_co + + ## GOOD : + # p.co = p.co @ mat.inverted() @ new_mat + + if o.parent: + o.matrix_world = new_mat + else: + o.matrix_basis = new_mat + + if reproject: + batch_reproject(o, project_type='FRONT') + + +def align_all_frames(reproject=True, ref=None): + + print('aligning all frames...') + + o = bpy.context.object + if not ref: + ref = bpy.context.scene.camera + + # get all rot + chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler' + + ## double list keys + rot_keys = [k.co.x for fcu in o.animation_data.action.fcurves for k in fcu.keyframe_points if fcu.data_path == chanel] + + ## normal iter + # for fcu in o.animation_data.action.fcurves: + # if fcu.data_path != chanel : + # continue + # for k in fcu.keyframe_points(): + # rot_keys.append(k.co.x) + + rot_keys = list(set(rot_keys)) + + # TODO # TOTHINK + # for now the rotation of the object is adjusted at every check.... + # might be better to check camera rotation of the current frame only stored as copy. + # else the object rotate following the camera... + + mat_90 = Matrix.Rotation(-pi/2, 4, 'X') + + for l in o.data.layers: + for f in l.frames: + # set the frame to dedicated + bpy.context.scene.frame_set(f.frame_number) + + ref_mat = ref.matrix_world + ref_loc, ref_rot, ref_scale = ref_mat.decompose() + + if o.parent: + mat = o.matrix_world + else: + mat = o.matrix_basis + + o_loc, o_rot, o_scale = mat.decompose() + loc_mat = Matrix.Translation(o_loc) + rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90 + scale_mat = get_scale_matrix(o_scale) + new_mat = loc_mat @ rot_mat @ scale_mat + + for s in f.strokes: + ## foreach + coords = [p.co @ mat.inverted() @ new_mat for p in s.points] + # print('coords: ', coords) + # print([co for v in coords for co in v]) + s.points.foreach_set('co', [co for v in coords for co in v]) + # s.points.update() # seem to works + ## force update + s.points.add(1) + s.points.pop() + + for fnum in rot_keys: + bpy.context.scene.frame_set(fnum) + #/update calculation block + ref_mat = ref.matrix_world + ref_loc, ref_rot, ref_scale = ref_mat.decompose() + + if o.parent: + mat = o.matrix_world + else: + mat = o.matrix_basis + + o_loc, o_rot, o_scale = mat.decompose() + loc_mat = Matrix.Translation(o_loc) + rot_mat = ref_rot.to_matrix().to_4x4() @ mat_90 + scale_mat = get_scale_matrix(o_scale) + new_mat = loc_mat @ rot_mat @ scale_mat + # update calculation block/ + + if o.parent: + o.matrix_world = new_mat + else: + o.matrix_basis = new_mat + + o.keyframe_insert(chanel, index=-1, frame=bpy.context.scene.frame_current, options={'INSERTKEY_AVAILABLE'}) + + + if reproject: + batch_reproject(o, project_type='FRONT') + + return + + +class GPTB_OT_realign(bpy.types.Operator): + bl_idname = "gp.realign" + bl_label = "Realign GP" + bl_description = "Realign the grease pencil on camera" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' + + reproject : bpy.props.BoolProperty(name='Reproject', default=True) + + def execute(self, context): + t0 = time() + oframe = context.scene.frame_current + + o = bpy.context.object + + if o.animation_data and o.animation_data.action: + if o.animation_data.action.fcurves.find('rotation_euler') or o.animation_data.action.fcurves.find('rotation_quaternion'): + align_all_frames(reproject=self.reproject) + + bpy.context.scene.frame_current = oframe + print(f'\nAnim realign ({time()-t0:.2f}s)') + return + + align_global(reproject=self.reproject) + + context.scene.frame_current = oframe + print(f'\nGlobal Realign ({time()-t0:.2f}s)') + + return {"FINISHED"} + + +class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator): + bl_idname = "gp.batch_reproject_all_frames" + bl_label = "Reproject All Frame" + bl_description = "Reproject all frame of active object." + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' + + reproject : bpy.props.BoolProperty(name='Reproject', default=True) + + # TODO witch side ? + + # def invoke(self, context, event): + # return context.window_manager.invoke_props_dialog(self) + # return self.execute(context) + + def draw(self, context): + self.layout.prop(self, "toggle", text="Toggle to redraw") + for i in range(20): + self.layout.label(str(i)) + + + def execute(self, context): + t0 = time() + batch_reproject(bpy.context.object, all_strokes=True, restore_frame=True) # all_strokes=True, restore_frame=False) + self.report({'INFO'}, f'\nReprojected in ({time()-t0:.2f}s)' ) + + return {"FINISHED"} + + +classes = ( +GPTB_OT_realign, +GPTB_OT_batch_reproject_all_frames +) + +def register(): + for cl in classes: + bpy.utils.register_class(cl) + +def unregister(): + for cl in reversed(classes): + bpy.utils.unregister_class(cl) \ No newline at end of file