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
parent
280a575631
commit
053a9d7f7b
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
112
OP_realign.py
112
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)' )
|
||||
|
||||
|
|
|
@ -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": "",
|
||||
|
|
50
utils.py
50
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]))
|
||||
|
||||
|
|
Loading…
Reference in New Issue