diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cd6075..fa52acb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +2.4.0 + +- changed: Batch reproject consider camera movement and is almost 8x faster +- added: Batch reproject have "Current" mode (using current tool setting) + 2.3.4 - fixed: bug when exporting json palettes containing empty material slots diff --git a/OP_material_picker.py b/OP_material_picker.py index cc88758..f78b6c5 100644 --- a/OP_material_picker.py +++ b/OP_material_picker.py @@ -8,7 +8,7 @@ from .utils import get_gp_draw_plane, location_to_region, region_to_location ### passing by 2D projection def get_3d_coord_on_drawing_plane_from_2d(context, co): - plane_co, plane_no = get_gp_draw_plane(context) + plane_co, plane_no = get_gp_draw_plane() rv3d = context.region_data view_mat = rv3d.view_matrix.inverted() if not plane_no: diff --git a/OP_realign.py b/OP_realign.py index 4c07f53..9b07765 100644 --- a/OP_realign.py +++ b/OP_realign.py @@ -4,7 +4,8 @@ from mathutils import Matrix, Vector from math import pi import numpy as np from time import time - +from . import utils +from mathutils.geometry import intersect_line_plane def get_scale_matrix(scale): # recreate a neutral mat scale @@ -21,64 +22,77 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False if restore_frame: oframe = bpy.context.scene.frame_current + + plan_co, plane_no = utils.get_gp_draw_plane(obj, orient=proj_type) + + frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.strokes)] + frame_list = list(set(frame_list)) + frame_list.sort() + + scn = bpy.context.scene + for i in frame_list: + scn.frame_set(i) # refresh scene + # scn.frame_current = i # no refresh + + origin = scn.camera.matrix_world.to_translation() + matrix_inv = obj.matrix_world.inverted() + # origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') + # matrix = np.array(obj.matrix_world, dtype='float64') + # matrix_inv = np.array(obj.matrix_world.inverted(), dtype='float64') + #mat = src.matrix_world + for l in obj.data.layers: + if not all_strokes: + if not l.select: + continue + if l.hide or l.lock: + continue + f = next((f for f in l.frames if f.frame_number == i), None) + if f is None: + continue + for s in f.strokes: + ## Batch matrix apply (Here is slower than list comprehension). + # nb_points = len(s.points) + # coords = np.empty(nb_points * 3, dtype='float64') + # s.points.foreach_get('co', coords) + # world_co_3d = utils.matrix_transform(coords.reshape((nb_points, 3)), matrix) + + ## list comprehension method + world_co_3d = [obj.matrix_world @ p.co for p in s.points] + + new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d] + + ## Basic method (Slower than foreach_set) + # for i, p in enumerate(s.points): + # p.co = obj.matrix_world.inverted() @ new_world_co_3d[i] + + ## Ravel new coordinate on the fly + new_local_coords = [axis for p in new_world_co_3d for axis in matrix_inv @ p] + + ## Set points in obj local space (apply matrix slower) + # new_local_coords = utils.matrix_transform(new_world_co_3d, matrix_inv).ravel() + s.points.foreach_set('co', new_local_coords) + + bpy.context.area.tag_redraw() + + ''' + ## Old method using Operators: 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') - frame_list = [f.frame_number for l in obj.data.layers for f in l.frames if len(f.strokes)] - frame_list = list(set(frame_list)) - frame_list.sort() for fnum in frame_list: bpy.context.scene.frame_current = fnum bpy.ops.gpencil.select_all(action='SELECT') bpy.ops.gpencil.reproject(type=proj_type) # 'INVOKE_DEFAULT' bpy.ops.gpencil.select_all(action='DESELECT') - - - #print('fnum: ', fnum) - # bpy.context.scene.frame_set(fnum) - # bpy.context.scene.frame_current = fnum - # bpy.ops.gpencil.select_all(action='SELECT') - # bpy.ops.gpencil.reproject(type=proj_type) # default is VIEW - # # bpy.ops.gpencil.select_all(action='DESELECT') - # bpy.ops.object.mode_set(mode='OBJECT') - # bpy.ops.object.mode_set(mode='EDIT_GPENCIL') - # bpy.context.view_layer.update() - - """ - for l in obj.data.layers: - for f in l.frames: - if not len(f.strokes): - continue - bpy.context.scene.frame_set(f.frame_number) - # bpy.context.scene.frame_current = f.frame_number - - ## / attempt update trigger for failing reproject surface mode - # bpy.ops.object.mode_set(mode='OBJECT') - # bpy.ops.object.mode_set(mode='EDIT_GPENCIL') - # bpy.context.view_layer.update() - # for a in bpy.context.screen.areas: - # a.tag_redraw() - # dg = bpy.context.evaluated_depsgraph_get() - # obj.evaluated_get(dg) - ## / - - # switch to edit to reproject through ops - bpy.ops.gpencil.select_all(action='SELECT') - bpy.ops.gpencil.reproject(type=proj_type) # default is VIEW - bpy.ops.gpencil.select_all(action='DESELECT') - """ - # restore if all_strokes: for layer, hide, lock, lock_frame in layers_state: @@ -87,6 +101,7 @@ def batch_reproject(obj, proj_type='VIEW', all_strokes=True, restore_frame=False layer.lock_frame = lock_frame bpy.ops.object.mode_set(mode=omode) + ''' if restore_frame: bpy.context.scene.frame_current = oframe @@ -355,14 +370,15 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator): description='Hided and locked layer will also be reprojected') type: bpy.props.EnumProperty(name='Type', - items=(('FRONT', "Front", ""), + items=(('CURRENT', "Current", ""), + ('FRONT', "Front", ""), ('SIDE', "Side", ""), ('TOP', "Top", ""), ('VIEW', "View", ""), ('SURFACE', "Surface", ""), ('CURSOR', "Cursor", ""), ), - default='FRONT') + default='CURRENT') def invoke(self, context, event): if context.object.data.use_multiedit: @@ -373,15 +389,19 @@ class GPTB_OT_batch_reproject_all_frames(bpy.types.Operator): def draw(self, context): layout = self.layout if not context.region_data.view_perspective == 'CAMERA': - layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR') + # layout.label(text='Not in camera ! (reprojection is made from view)', icon='ERROR') + layout.label(text='Reprojection is made from camera, not current view', icon='ERROR') layout.prop(self, "all_strokes") layout.prop(self, "type") def execute(self, context): t0 = time() + orient = self.type + if self.type == 'CURRENT': + orient = None - batch_reproject(context.object, proj_type=self.type, all_strokes=self.all_strokes, restore_frame=True) + batch_reproject(context.object, proj_type=orient, all_strokes=self.all_strokes, restore_frame=True) self.report({'INFO'}, f'Reprojected in ({time()-t0:.2f}s)' ) diff --git a/__init__.py b/__init__.py index 580d370..ad82d75 100755 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "GP toolbox", "description": "Tool set for Grease Pencil in animation production", "author": "Samuel Bernou, Christophe Seux", -"version": (2, 3, 4), +"version": (2, 4, 0), "blender": (3, 0, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "", diff --git a/utils.py b/utils.py index c68f286..8db8c71 100644 --- a/utils.py +++ b/utils.py @@ -263,10 +263,12 @@ def remapping(value, leftMin, leftMax, rightMin, rightMax): ### GP funcs # ----------------- -def get_gp_draw_plane(context, obj=None): +""" V1 +def get_gp_draw_plane(obj=None): ''' return tuple with plane coordinate and normal of the curent drawing accordign to geometry''' + context = bpy.context settings = context.scene.tool_settings orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR' loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE' @@ -307,6 +309,46 @@ def get_gp_draw_plane(context, obj=None): plane_no.rotate(context.scene.cursor.matrix) return plane_co, plane_no +""" + +## V2 +def get_gp_draw_plane(obj=None, orient=None): + ''' return tuple with plane coordinate and normal + of the curent drawing according to geometry''' + + if obj is None: + obj = bpy.context.object + + settings = bpy.context.scene.tool_settings + if orient is None: + orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR' + loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE' + + mat = obj.matrix_world + plane_no = Vector((0.0, 0.0, 1.0)) + plane_co = mat.to_translation() + + # -> orientation + if orient == 'VIEW': + mat = bpy.context.scene.camera.matrix_world + # -> placement + if loc == "CURSOR": + plane_co = bpy.context.scene.cursor.location + mat = bpy.context.scene.cursor.matrix + + elif orient == 'AXIS_Y':#front (X-Z) + plane_no = Vector((0,1,0)) + + elif orient == 'AXIS_X':#side (Y-Z) + plane_no = Vector((1,0,0)) + + elif orient == 'AXIS_Z':#top (X-Y) + plane_no = Vector((0,0,1)) + + plane_no.rotate(mat) + + return plane_co, plane_no + def check_angle_from_view(obj=None, plane_no=None, context=None): '''Return angle to obj according to chosen drawing axis''' @@ -317,7 +359,7 @@ def check_angle_from_view(obj=None, plane_no=None, context=None): context = bpy.context if not plane_no: - _plane_co, plane_no = get_gp_draw_plane(context, obj=obj) + _plane_co, plane_no = get_gp_draw_plane(obj=obj) view_direction = view3d_utils.region_2d_to_vector_3d(context.region, context.region_data, (context.region.width/2.0, context.region.height/2.0)) angle = math.degrees(view_direction.angle(plane_no)) @@ -498,6 +540,10 @@ from mathutils import Vector ### Vector utils 3d # ----------------- +def matrix_transform(coords, matrix): + coords_4d = np.column_stack((coords, np.ones(len(coords), dtype='float64'))) + return np.einsum('ij,aj->ai', matrix, coords_4d)[:, :-1] + def single_vector_length(v): return sqrt((v[0] * v[0]) + (v[1] * v[1]) + (v[2] * v[2]))