Compare commits

...

8 Commits

Author SHA1 Message Date
48eded1a9b feat: update replay 2022-01-21 09:06:56 +01:00
3fbe769928 feat: groundd work on legacy snapshots loading 2021-12-10 16:57:03 +01:00
fe998214be feat: replay duration 2021-12-08 17:42:20 +01:00
3a4f691b8f feat: previous message 2021-12-08 16:30:56 +01:00
0e20d35e7d refactor: move clean to operator.py
feat: timeline action mapping for replay
feat: persistent  collection
fix: draw users in the right scene according to the snapshot
2021-12-08 16:30:40 +01:00
8d15e69b50 fix: replay count 2021-11-24 19:12:29 +01:00
8000ce9931 feat: basic file sequence loading 2021-11-24 18:56:17 +01:00
b96f600f15 feat: initial inteface 2021-11-18 18:01:35 +01:00
5 changed files with 347 additions and 45 deletions

View File

@ -130,14 +130,29 @@ def load_pre_handler(dummy):
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
bpy.ops.session.stop()
@persistent
def update_client_frame(scene):
setting = bpy.context.window_manager.session
if setting.replay_mode == 'TIMELINE' and \
setting.replay_files and \
scene.active_replay_file != setting.replay_frame_current :
index = bpy.context.scene.active_replay_file
bpy.ops.session.load(filepath=bpy.context.window_manager.session.replay_files[index].name,
draw_users=True,
replay=True)
setting.replay_frame_current = index
if session and session.state == STATE_ACTIVE:
porcelain.update_user_metadata(session.repository, {
'frame_current': scene.frame_current
})
@persistent
def post_frame_update(scene):
if bpy.context.window_manager.session.replay_mode == 'TIMELINE' and \
not bpy.context.scene.animation_data:
bpy.context.scene.animation_data_create()
bpy.context.scene.animation_data.action = bpy.data.actions.get('replay_action')
def register():
bpy.app.handlers.undo_post.append(resolve_deps_graph)

View File

@ -35,6 +35,7 @@ from operator import itemgetter
from pathlib import Path
from queue import Queue
from time import gmtime, strftime
from numpy import interp
from bpy.props import FloatProperty
import bmesh
@ -67,13 +68,51 @@ deleyables = []
stop_modal_executor = False
CLEARED_DATABLOCKS = ['actions', 'armatures', 'cache_files', 'cameras',
'collections', 'curves', 'fonts',
'grease_pencils', 'images', 'lattices', 'libraries',
'lightprobes', 'lights', 'linestyles', 'masks',
'materials', 'meshes', 'metaballs', 'movieclips',
'node_groups', 'objects', 'paint_curves', 'particles',
'scenes', 'shape_keys', 'sounds', 'speakers', 'texts',
'textures', 'volumes', 'worlds']
PERSISTENT_DATABLOCKS = ['LineStyle', 'Dots Stroke', 'replay_action']
def clean_scene(ignored_datablocks: list = None):
"""
Delete all datablock of the scene except PERSISTENT_DATABLOCKS and ignored
ones in ignored_datablocks.
"""
PERSISTENT_DATABLOCKS.extend(ignored_datablocks)
# Avoid to trigger a runtime error by keeping the last scene
PERSISTENT_DATABLOCKS.append(bpy.data.scenes[0].name)
for type_name in CLEARED_DATABLOCKS:
type_collection = getattr(bpy.data, type_name)
for datablock in type_collection:
if datablock.name in PERSISTENT_DATABLOCKS:
logging.debug(f"Skipping {datablock.name}")
continue
else:
logging.debug(f"Removing {datablock.name}")
type_collection.remove(datablock)
# Clear sequencer
bpy.context.scene.sequence_editor_clear()
def draw_user(username, metadata, radius=0.01, intensity=10.0):
"""
Generate a mesh representation of a given user frustum and
sight of view.
"""
view_corners = metadata.get('view_corners')
color = metadata.get('color', (1,1,1,0))
objects = metadata.get('selected_objects', None)
scene = metadata.get('scene_current', bpy.context.scene.name)
user_collection = bpy.data.collections.new(username)
# User Color
user_mat = bpy.data.materials.new(username)
user_mat.use_nodes = True
@ -115,7 +154,7 @@ def draw_user(username, metadata, radius=0.01, intensity=10.0):
camera_obj.modifiers.new("wireframe", "SKIN")
camera_obj.data.skin_vertices[0].data[0].use_root = True
for v in camera_mesh.skin_vertices[0].data:
for v in camera_obj.data.skin_vertices[0].data:
v.radius = [radius, radius]
camera_mesh.materials.append(user_mat)
@ -176,7 +215,7 @@ def draw_user(username, metadata, radius=0.01, intensity=10.0):
bbox_mesh.materials.append(user_mat)
bpy.context.scene.collection.children.link(user_collection)
bpy.data.scenes[scene].collection.children.link(user_collection)
def session_callback(name):
@ -363,7 +402,7 @@ class SessionConnectOperator(bpy.types.Operator):
# Join a session
if not active_server.use_admin_password:
utils.clean_scene()
clean_scene()
try:
porcelain.remote_add(
@ -434,7 +473,7 @@ class SessionHostOperator(bpy.types.Operator):
# Host a session
if settings.init_method == 'EMPTY':
utils.clean_scene()
clean_scene()
try:
# Init repository
@ -498,7 +537,7 @@ class SessionInitOperator(bpy.types.Operator):
def execute(self, context):
if self.init_method == 'EMPTY':
utils.clean_scene()
clean_scene()
for scene in bpy.data.scenes:
porcelain.add(session.repository, scene)
@ -982,28 +1021,84 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
description="Draw users in the scene",
default=False,
)
replay: bpy.props.BoolProperty(
name="Replay mode",
description="Enable replay functions",
default=False,
)
user_skin_radius: bpy.props.FloatProperty(
name="Wireframe radius",
description="Wireframe radius",
default=0.005,
default=0.01,
)
user_color_intensity: bpy.props.FloatProperty(
name="Shading intensity",
description="Shading intensity",
default=10.0,
default=1.0,
)
files: bpy.props.CollectionProperty(
name='File paths',
type=bpy.types.OperatorFileListElement
)
def draw(self, context):
pass
def execute(self, context):
from replication.repository import Repository
runtime_settings = context.window_manager.session
# init the factory with supported types
bpy_protocol = bl_types.get_data_translation_protocol()
repo = Repository(bpy_protocol)
repo.loads(self.filepath)
utils.clean_scene()
try:
repo.loads(self.filepath)
except TypeError:
# Load legacy snapshots
db = pickle.load(gzip.open(self.filepath, "rb"))
nodes = db.get("nodes")
logging.info(f"Loading legacy {len(nodes)} node")
repo.object_store.clear()
for node, node_data in nodes:
instance = Node(
uuid=node,
data=node_data.get('data'),
owner=node_data.get('owner'),
dependencies=node_data.get('dependencies'),
state=FETCHED)
# Patch data for compatibility
type_id = node_data.get('str_type')[2:]
if type_id == "File":
type_id = "WindowsPath"
instance.data['type_id'] = type_id
repo.do_commit(instance)
instance.state = FETCHED
# Persitstent collection
ignored_datablocks = []
persistent_collection = bpy.data.collections.get("multiuser_timelapse")
if self.replay and \
runtime_settings.replay_persistent_collection and \
persistent_collection:
collection_repo = Repository(
rdp=bpy_protocol,
username="None")
porcelain.add(collection_repo, persistent_collection)
porcelain.commit(collection_repo, persistent_collection.uuid)
for node in collection_repo.graph.values():
ignored_datablocks.append(node.data.get('name'))
clean_scene(ignored_datablocks=ignored_datablocks)
nodes = [repo.graph.get(n) for n in repo.index_sorted]
@ -1018,6 +1113,37 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
for node in nodes:
porcelain.apply(repo, node.uuid)
if len(self.files) > 1:
runtime_settings.replay_files.clear()
context.scene.active_replay_file = len(self.files)-1
directory = Path(self.filepath).parent
file_list = [f['name'] for f in self.files]
file_list.sort()
for f in file_list:
snap = runtime_settings.replay_files.add()
snap.name = str(Path(directory, f))
print(f)
if runtime_settings.replay_mode == 'TIMELINE':
replay_action = bpy.data.actions.get('replay_action', bpy.data.actions.new('replay_action'))
bpy.context.scene.animation_data_create()
bpy.context.scene.animation_data.action = replay_action
if len(replay_action.fcurves) > 0 and replay_action.fcurves[0].data_path == 'active_replay_file':
replay_fcurve = replay_action.fcurves[0]
else:
replay_fcurve = replay_action.fcurves.new('active_replay_file')
for p in reversed(replay_fcurve.keyframe_points):
replay_fcurve.keyframe_points.remove(p, fast=True)
duration = runtime_settings.replay_duration
file_count = len(self.files)-1
for index in range(0, file_count):
frame = interp(index, [0, file_count], [bpy.context.scene.frame_start, duration])
replay_fcurve.keyframe_points.insert(frame, index)
if self.draw_users:
f = gzip.open(self.filepath, "rb")
db = pickle.load(f)
@ -1030,13 +1156,34 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
if metadata:
draw_user(username, metadata, radius=self.user_skin_radius, intensity=self.user_color_intensity)
# Relink the persistent collection
if self.replay and persistent_collection:
logging.info(f"Relinking {persistent_collection.name}")
bpy.context.scene.collection.children.link(persistent_collection)
# Reasign scene action
if self.replay and \
runtime_settings.replay_mode == 'TIMELINE' and \
not bpy.context.scene.animation_data :
bpy.context.scene.animation_data_create()
bpy.context.scene.animation_data.action = bpy.data.actions.get('replay_action')
bpy.context.scene.frame_end = runtime_settings.replay_duration
# Reasign the scene camera
if self.replay and \
runtime_settings.replay_persistent_collection and \
runtime_settings.replay_camera:
bpy.context.scene.camera = runtime_settings.replay_camera
return {'FINISHED'}
@classmethod
def poll(cls, context):
return True
class SessionImportUser(bpy.types.Panel):
class SESSION_PT_ImportUser(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Users"
@ -1283,6 +1430,26 @@ def menu_func_import(self, context):
def menu_func_export(self, context):
self.layout.operator(SessionSaveBackupOperator.bl_idname, text='Multi-user session snapshot (.db)')
class SessionRenderReplay(bpy.types.Operator):
bl_idname = "session.render_replay"
bl_label = "Render Replay"
bl_description = "Render Replay"
@classmethod
def poll(cls, context):
return context.window_manager.session.replay_files
def execute(self, context):
base_path = str(context.scene.render.filepath)
for frame in range(0,context.scene.frame_end):
logging.info(f"Rendering frame {frame} to {base_path}_{frame}.png")
context.scene.frame_current = frame
filename = Path(bpy.context.window_manager.session.replay_files[context.scene.active_replay_file].name)
context.scene.render.filepath = f"{base_path}{frame}_{filename.stem}"
bpy.ops.render.render(write_still=True)
context.scene.render.filepath = base_path
return {'FINISHED'}
classes = (
SessionConnectOperator,
@ -1300,7 +1467,7 @@ classes = (
SessionNotifyOperator,
SessionSaveBackupOperator,
SessionLoadSaveOperator,
SessionImportUser,
SESSION_PT_ImportUser,
SessionStopAutoSaveOperator,
SessionPurgeOperator,
SessionPresetServerAdd,
@ -1309,6 +1476,7 @@ classes = (
RefreshServerStatus,
GetDoc,
FirstLaunch,
SessionRenderReplay,
)

View File

@ -22,6 +22,7 @@ import bpy
import string
import re
import os
from numpy import interp
from pathlib import Path
@ -99,6 +100,77 @@ def update_directory(self, context):
def set_log_level(self, value):
logging.getLogger().setLevel(value)
def set_active_replay(self, value):
files_count = len(bpy.context.window_manager.session.replay_files)
if files_count == 0:
return
max_index = files_count-1
if value > max_index:
value = max_index
if hasattr(self, 'active_replay_file'):
self["active_replay_file"] = value
else:
self.active_replay_file = value
if bpy.context.window_manager.session.replay_mode == 'MANUAL':
bpy.ops.session.load(
filepath=bpy.context.window_manager.session.replay_files[value].name,
draw_users=True,
replay=True)
def get_active_replay(self):
return self.get('active_replay_file', 0)
def set_replay_persistent_collection(self, value):
if hasattr(self, 'replay_persistent_collection'):
self["replay_persistent_collection"] = value
else:
self.replay_persistent_collection = value
collection = bpy.data.collections.get("multiuser_timelapse", None)
if collection is None and value:
collection = bpy.data.collections.new('multiuser_timelapse')
bpy.context.scene.collection.children.link(collection)
elif collection and not value:
for o in collection.objects:
bpy.data.objects.remove(o)
bpy.data.collections.remove(collection)
def get_replay_persistent_collection(self):
return self.get('replay_persistent_collection', False)
def set_replay_duration(self, value):
if hasattr(self, 'replay_duration'):
self["replay_duration"] = value
else:
self.replay_duration = value
# Update the animation fcurve
replay_action = bpy.data.actions.get('replay_action')
replay_fcurve = None
for fcurve in replay_action.fcurves:
if fcurve.data_path == 'active_replay_file':
replay_fcurve = fcurve
if replay_fcurve:
for p in reversed(replay_fcurve.keyframe_points):
replay_fcurve.keyframe_points.remove(p, fast=True)
bpy.context.scene.frame_end = value
files_count = len(bpy.context.window_manager.session.replay_files)-1
for index in range(0, files_count):
frame = interp(index,[0, files_count],[bpy.context.scene.frame_start, value])
replay_fcurve.keyframe_points.insert(frame, index)
def get_replay_duration(self):
return self.get('replay_duration', 10)
def get_log_level(self):
return logging.getLogger().level
@ -687,6 +759,37 @@ class SessionProps(bpy.types.PropertyGroup):
is_host: bpy.props.BoolProperty(
default=False
)
replay_files: bpy.props.CollectionProperty(
name='File paths',
type=bpy.types.OperatorFileListElement
)
replay_persistent_collection: bpy.props.BoolProperty(
name="replay_persistent_collection",
description='Enable a collection that persist accross frames loading',
get=get_replay_persistent_collection,
set=set_replay_persistent_collection,
)
replay_mode: bpy.props.EnumProperty(
name='replay method',
description='Replay in keyframe (timeline) or manually',
items={
('TIMELINE', 'TIMELINE', 'Replay from the timeline.'),
('MANUAL', 'MANUAL', 'Replay manually, from the replay frame widget.')},
default='TIMELINE')
replay_duration: bpy.props.IntProperty(
name='replay interval',
default=250,
min=10,
set=set_replay_duration,
get=get_replay_duration,
)
replay_frame_current: bpy.props.IntProperty(
name='replay_frame_current',
)
replay_camera: bpy.props.PointerProperty(
name='Replay camera',
type=bpy.types.Object
)
classes = (
@ -712,6 +815,16 @@ def register():
# at launch server presets
prefs.generate_default_presets()
bpy.types.Scene.active_replay_file = bpy.props.IntProperty(
name="active_replay_file",
default=0,
min=0,
description='Active snapshot',
set=set_active_replay,
get=get_active_replay,
options={'ANIMATABLE'}
)
@ -720,3 +833,5 @@ def unregister():
for cls in reversed(classes):
unregister_class(cls)
del bpy.types.Scene.active_replay_file

View File

@ -564,10 +564,42 @@ class SESSION_PT_sync(bpy.types.Panel):
row.prop(settings.sync_flags, "sync_active_camera", text="",icon_only=True, icon='VIEW_CAMERA')
class SESSION_PT_replay(bpy.types.Panel):
bl_idname = "MULTIUSER_REPLAY_PT_panel"
bl_label = "Replay"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.window_manager.session.replay_files
def draw_header(self, context):
self.layout.label(text="", icon='RECOVER_LAST')
def draw(self, context):
layout = self.layout
settings = context.window_manager.session
row= layout.row()
row.prop(settings,'replay_mode', toggle=True, expand=True)
row= layout.row()
if settings.replay_mode == 'MANUAL':
row.prop(bpy.context.scene, 'active_replay_file', text="Snapshot index")
else:
row.prop(settings, 'replay_duration', text="Replay Duration")
row= layout.row()
row.prop(settings, 'replay_persistent_collection', text="persistent collection", toggle=True, icon='OUTLINER_COLLECTION')
if settings.replay_persistent_collection:
row= layout.row()
row.prop(settings, 'replay_camera', text="", icon='VIEW_CAMERA')
class SESSION_PT_repository(bpy.types.Panel):
bl_idname = "MULTIUSER_PROPERTIES_PT_panel"
bl_label = "Repository"
bl_space_type = 'VIEW_3D'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
bl_options = {'DEFAULT_CLOSED'}
@ -709,6 +741,7 @@ classes = (
SESSION_PT_sync,
SESSION_PT_repository,
VIEW3D_PT_overlay_session,
SESSION_PT_replay,
)
register, unregister = bpy.utils.register_classes_factory(classes)

View File

@ -38,15 +38,6 @@ from replication.constants import (STATE_ACTIVE, STATE_AUTH,
STATE_LOBBY,
CONNECTING)
CLEARED_DATABLOCKS = ['actions', 'armatures', 'cache_files', 'cameras',
'collections', 'curves', 'filepath', 'fonts',
'grease_pencils', 'images', 'lattices', 'libraries',
'lightprobes', 'lights', 'linestyles', 'masks',
'materials', 'meshes', 'metaballs', 'movieclips',
'node_groups', 'objects', 'paint_curves', 'particles',
'scenes', 'shape_keys', 'sounds', 'speakers', 'texts',
'textures', 'volumes', 'worlds']
def find_from_attr(attr_name, attr_value, list):
for item in list:
if getattr(item, attr_name, None) == attr_value:
@ -108,26 +99,6 @@ def get_state_str(state):
return state_str
def clean_scene():
for type_name in CLEARED_DATABLOCKS:
sub_collection_to_avoid = [
bpy.data.linestyles.get('LineStyle'),
bpy.data.materials.get('Dots Stroke')
]
type_collection = getattr(bpy.data, type_name)
items_to_remove = [i for i in type_collection if i not in sub_collection_to_avoid]
for item in items_to_remove:
try:
type_collection.remove(item)
logging.info(item.name)
except:
continue
# Clear sequencer
bpy.context.scene.sequence_editor_clear()
def get_selected_objects(scene, active_view_layer):
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]