Faster and better batch reproject

2.4.0

- changed: Batch reproject consider camera movement and is almost 8x faster
- added: Batch reproject have "Current" mode (using current tool setting)
gpv2
pullusb 2024-01-18 19:24:52 +01:00
parent 280a575631
commit 053a9d7f7b
5 changed files with 121 additions and 50 deletions

View File

@ -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

View File

@ -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:

View File

@ -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)' )

View File

@ -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": "",

View File

@ -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]))