feat: replay
This commit is contained in:
parent
57fdd492ef
commit
8cb40b2d60
@ -32,6 +32,7 @@ from operator import itemgetter
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
|
from numpy import interp
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import _pickle as pickle
|
import _pickle as pickle
|
||||||
@ -39,6 +40,7 @@ except ImportError:
|
|||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import bmesh
|
||||||
import mathutils
|
import mathutils
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||||
@ -56,6 +58,226 @@ background_execution_queue = Queue()
|
|||||||
deleyables = []
|
deleyables = []
|
||||||
stop_modal_executor = False
|
stop_modal_executor = False
|
||||||
|
|
||||||
|
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']
|
||||||
|
|
||||||
|
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 get_bb_coords_from_obj(object: bpy.types.Object, instance: bpy.types.Object = None) -> list:
|
||||||
|
""" Generate bounding box in world coordinate from object bound box
|
||||||
|
|
||||||
|
:param object: target object
|
||||||
|
:type object: bpy.types.Object
|
||||||
|
:param instance: optionnal instance
|
||||||
|
:type instance: bpy.types.Object
|
||||||
|
:return: list of 8 points [(x,y,z),...]
|
||||||
|
"""
|
||||||
|
base = object.matrix_world
|
||||||
|
|
||||||
|
if instance:
|
||||||
|
scale = mathutils.Matrix.Diagonal(object.matrix_world.to_scale())
|
||||||
|
base = instance.matrix_world @ scale.to_4x4()
|
||||||
|
|
||||||
|
bbox_corners = [base @ mathutils.Vector(
|
||||||
|
corner) for corner in object.bound_box]
|
||||||
|
|
||||||
|
|
||||||
|
return [(point.x, point.y, point.z) for point in bbox_corners]
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_from_obj(obj: bpy.types.Object, index: int = 1) -> list:
|
||||||
|
""" Generate a bounding box for a given object by using its world matrix
|
||||||
|
|
||||||
|
:param obj: target object
|
||||||
|
:type obj: bpy.types.Object
|
||||||
|
:param index: indice offset
|
||||||
|
:type index: int
|
||||||
|
:return: list of 8 points [(x,y,z),...], list of 12 link between these points [(1,2),...]
|
||||||
|
"""
|
||||||
|
radius = 1.0 # Radius of the bounding box
|
||||||
|
index = 8*index
|
||||||
|
vertex_indices = (
|
||||||
|
(0+index, 1+index), (0+index, 2+index), (1+index, 3+index), (2+index, 3+index),
|
||||||
|
(4+index, 5+index), (4+index, 6+index), (5+index, 7+index), (6+index, 7+index),
|
||||||
|
(0+index, 4+index), (1+index, 5+index), (2+index, 6+index), (3+index, 7+index))
|
||||||
|
|
||||||
|
if obj.type == 'EMPTY':
|
||||||
|
radius = obj.empty_display_size
|
||||||
|
elif obj.type == 'LIGHT':
|
||||||
|
radius = obj.data.shadow_soft_size
|
||||||
|
elif obj.type == 'LIGHT_PROBE':
|
||||||
|
radius = obj.data.influence_distance
|
||||||
|
elif obj.type == 'CAMERA':
|
||||||
|
radius = obj.data.display_size
|
||||||
|
elif hasattr(obj, 'bound_box'):
|
||||||
|
vertex_indices = (
|
||||||
|
(0+index, 1+index), (1+index, 2+index),
|
||||||
|
(2+index, 3+index), (0+index, 3+index),
|
||||||
|
(4+index, 5+index), (5+index, 6+index),
|
||||||
|
(6+index, 7+index), (4+index, 7+index),
|
||||||
|
(0+index, 4+index), (1+index, 5+index),
|
||||||
|
(2+index, 6+index), (3+index, 7+index))
|
||||||
|
vertex_pos = get_bb_coords_from_obj(obj)
|
||||||
|
return vertex_pos, vertex_indices
|
||||||
|
|
||||||
|
coords = [
|
||||||
|
(-radius, -radius, -radius), (+radius, -radius, -radius),
|
||||||
|
(-radius, +radius, -radius), (+radius, +radius, -radius),
|
||||||
|
(-radius, -radius, +radius), (+radius, -radius, +radius),
|
||||||
|
(-radius, +radius, +radius), (+radius, +radius, +radius)]
|
||||||
|
|
||||||
|
base = obj.matrix_world
|
||||||
|
bbox_corners = [base @ mathutils.Vector(corner) for corner in coords]
|
||||||
|
|
||||||
|
vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners]
|
||||||
|
|
||||||
|
return vertex_pos, vertex_indices
|
||||||
|
|
||||||
|
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
|
||||||
|
nodes = user_mat.node_tree.nodes
|
||||||
|
nodes.remove(nodes['Principled BSDF'])
|
||||||
|
emission_node = nodes.new('ShaderNodeEmission')
|
||||||
|
emission_node.inputs['Color'].default_value = color
|
||||||
|
emission_node.inputs['Strength'].default_value = intensity
|
||||||
|
|
||||||
|
output_node = nodes['Material Output']
|
||||||
|
user_mat.node_tree.links.new(
|
||||||
|
emission_node.outputs['Emission'], output_node.inputs['Surface'])
|
||||||
|
|
||||||
|
# Generate camera mesh
|
||||||
|
camera_vertices = view_corners[:4]
|
||||||
|
camera_vertices.append(view_corners[6])
|
||||||
|
camera_mesh = bpy.data.meshes.new(f"{username}_camera")
|
||||||
|
camera_obj = bpy.data.objects.new(f"{username}_camera", camera_mesh)
|
||||||
|
frustum_bm = bmesh.new()
|
||||||
|
frustum_bm.from_mesh(camera_mesh)
|
||||||
|
|
||||||
|
for p in camera_vertices:
|
||||||
|
frustum_bm.verts.new(p)
|
||||||
|
frustum_bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[0], frustum_bm.verts[2]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[2], frustum_bm.verts[1]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[1], frustum_bm.verts[3]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[3], frustum_bm.verts[0]))
|
||||||
|
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[0], frustum_bm.verts[4]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[2], frustum_bm.verts[4]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[1], frustum_bm.verts[4]))
|
||||||
|
frustum_bm.edges.new((frustum_bm.verts[3], frustum_bm.verts[4]))
|
||||||
|
frustum_bm.edges.ensure_lookup_table()
|
||||||
|
|
||||||
|
frustum_bm.to_mesh(camera_mesh)
|
||||||
|
frustum_bm.free() # free and prevent further access
|
||||||
|
|
||||||
|
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:
|
||||||
|
v.radius = [radius, radius]
|
||||||
|
|
||||||
|
camera_mesh.materials.append(user_mat)
|
||||||
|
user_collection.objects.link(camera_obj)
|
||||||
|
|
||||||
|
# Generate sight mesh
|
||||||
|
sight_mesh = bpy.data.meshes.new(f"{username}_sight")
|
||||||
|
sight_obj = bpy.data.objects.new(f"{username}_sight", sight_mesh)
|
||||||
|
sight_verts = view_corners[4:6]
|
||||||
|
sight_bm = bmesh.new()
|
||||||
|
sight_bm.from_mesh(sight_mesh)
|
||||||
|
|
||||||
|
for p in sight_verts:
|
||||||
|
sight_bm.verts.new(p)
|
||||||
|
sight_bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
sight_bm.edges.new((sight_bm.verts[0], sight_bm.verts[1]))
|
||||||
|
sight_bm.edges.ensure_lookup_table()
|
||||||
|
sight_bm.to_mesh(sight_mesh)
|
||||||
|
sight_bm.free()
|
||||||
|
|
||||||
|
sight_obj.modifiers.new("wireframe", "SKIN")
|
||||||
|
sight_obj.data.skin_vertices[0].data[0].use_root = True
|
||||||
|
for v in sight_mesh.skin_vertices[0].data:
|
||||||
|
v.radius = [radius, radius]
|
||||||
|
|
||||||
|
sight_mesh.materials.append(user_mat)
|
||||||
|
user_collection.objects.link(sight_obj)
|
||||||
|
|
||||||
|
# Draw selected objects
|
||||||
|
if objects:
|
||||||
|
for o in list(objects):
|
||||||
|
instance = bl_types.bl_datablock.get_datablock_from_uuid(o, None)
|
||||||
|
if instance:
|
||||||
|
bbox_mesh = bpy.data.meshes.new(f"{instance.name}_bbox")
|
||||||
|
bbox_obj = bpy.data.objects.new(
|
||||||
|
f"{instance.name}_bbox", bbox_mesh)
|
||||||
|
bbox_verts, bbox_ind = bbox_from_obj(instance, index=0)
|
||||||
|
bbox_bm = bmesh.new()
|
||||||
|
bbox_bm.from_mesh(bbox_mesh)
|
||||||
|
|
||||||
|
for p in bbox_verts:
|
||||||
|
bbox_bm.verts.new(p)
|
||||||
|
bbox_bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
for e in bbox_ind:
|
||||||
|
bbox_bm.edges.new(
|
||||||
|
(bbox_bm.verts[e[0]], bbox_bm.verts[e[1]]))
|
||||||
|
|
||||||
|
bbox_bm.to_mesh(bbox_mesh)
|
||||||
|
bbox_bm.free()
|
||||||
|
bpy.data.collections[username].objects.link(bbox_obj)
|
||||||
|
|
||||||
|
bbox_obj.modifiers.new("wireframe", "SKIN")
|
||||||
|
bbox_obj.data.skin_vertices[0].data[0].use_root = True
|
||||||
|
for v in bbox_mesh.skin_vertices[0].data:
|
||||||
|
v.radius = [radius, radius]
|
||||||
|
|
||||||
|
bbox_mesh.materials.append(user_mat)
|
||||||
|
|
||||||
|
bpy.data.scenes[scene].collection.children.link(user_collection)
|
||||||
|
|
||||||
|
|
||||||
def session_callback(name):
|
def session_callback(name):
|
||||||
""" Session callback wrapper
|
""" Session callback wrapper
|
||||||
|
|
||||||
@ -827,9 +1049,42 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
|||||||
maxlen=255, # Max internal buffer length, longer would be clamped.
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
draw_users: bpy.props.BoolProperty(
|
||||||
|
name="Load users",
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
user_color_intensity: bpy.props.FloatProperty(
|
||||||
|
name="Shading intensity",
|
||||||
|
description="Shading intensity",
|
||||||
|
default=1.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
files: bpy.props.CollectionProperty(
|
||||||
|
name='File paths',
|
||||||
|
type=bpy.types.OperatorFileListElement
|
||||||
|
)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
pass
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
from replication.graph import ReplicationGraph
|
from replication.graph import ReplicationGraph
|
||||||
|
|
||||||
|
runtime_settings = context.window_manager.session
|
||||||
|
|
||||||
# TODO: add filechecks
|
# TODO: add filechecks
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -878,7 +1133,16 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
|||||||
|
|
||||||
logging.info("Graph succefully loaded")
|
logging.info("Graph succefully loaded")
|
||||||
|
|
||||||
utils.clean_scene()
|
# Persitstent collection
|
||||||
|
ignored_datablocks = []
|
||||||
|
|
||||||
|
persistent_collection = bpy.data.collections.get("multiuser_timelapse")
|
||||||
|
if self.replay and \
|
||||||
|
runtime_settings.replay_persistent_collection and \
|
||||||
|
persistent_collection:
|
||||||
|
ignored_datablocks = ['multiuser_timelapse','multiuser_timelapse_cam','multiuser_timelapse_cam_obj','multiuser_timelapse_path','multiuser_timelapse_path_obj', 'multiuser_timelapse_pathAction']
|
||||||
|
|
||||||
|
clean_scene(ignored_datablocks=ignored_datablocks)
|
||||||
|
|
||||||
# Step 1: Construct nodes
|
# Step 1: Construct nodes
|
||||||
for node in graph.list_ordered():
|
for node in graph.list_ordered():
|
||||||
@ -888,6 +1152,68 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
|||||||
for node in graph.list_ordered():
|
for node in graph.list_ordered():
|
||||||
graph[node].apply()
|
graph[node].apply()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
users = db.get("users")
|
||||||
|
|
||||||
|
for username, user_data in users.items():
|
||||||
|
metadata = user_data['metadata']
|
||||||
|
|
||||||
|
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'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
@ -967,6 +1293,16 @@ def load_pre_handler(dummy):
|
|||||||
|
|
||||||
@persistent
|
@persistent
|
||||||
def update_client_frame(scene):
|
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'] == STATE_ACTIVE:
|
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||||
session.update_user_metadata({
|
session.update_user_metadata({
|
||||||
'frame_current': scene.frame_current
|
'frame_current': scene.frame_current
|
||||||
|
@ -28,7 +28,7 @@ from . import bl_types, environment, addon_updater_ops, presence, ui
|
|||||||
from .utils import get_preferences, get_expanded_icon
|
from .utils import get_preferences, get_expanded_icon
|
||||||
from replication.constants import RP_COMMON
|
from replication.constants import RP_COMMON
|
||||||
from replication.interface import session
|
from replication.interface import session
|
||||||
|
from numpy import interp
|
||||||
# From https://stackoverflow.com/a/106223
|
# From https://stackoverflow.com/a/106223
|
||||||
IP_REGEX = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
|
IP_REGEX = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
|
||||||
HOSTNAME_REGEX = re.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")
|
HOSTNAME_REGEX = re.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")
|
||||||
@ -93,6 +93,88 @@ def set_log_level(self, value):
|
|||||||
def get_log_level(self):
|
def get_log_level(self):
|
||||||
return logging.getLogger().level
|
return logging.getLogger().level
|
||||||
|
|
||||||
|
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')
|
||||||
|
cam = bpy.data.cameras.get('multiuser_timelapse_cam', bpy.data.cameras.new('multiuser_timelapse_cam'))
|
||||||
|
cam_obj = bpy.data.objects.get('multiuser_timelapse_cam_obj', bpy.data.objects.new('multiuser_timelapse_cam_obj', cam))
|
||||||
|
curve = bpy.data.curves.get('multiuser_timelapse_path', bpy.data.curves.new('multiuser_timelapse_path', 'CURVE'))
|
||||||
|
curve_obj = bpy.data.objects.get('multiuser_timelapse_path_obj', bpy.data.objects.new('multiuser_timelapse_path_obj', curve))
|
||||||
|
|
||||||
|
if cam_obj.name not in collection.objects:
|
||||||
|
collection.objects.link(cam_obj)
|
||||||
|
if curve_obj.name not in collection.objects:
|
||||||
|
collection.objects.link(curve_obj)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class ReplicatedDatablock(bpy.types.PropertyGroup):
|
class ReplicatedDatablock(bpy.types.PropertyGroup):
|
||||||
type_name: bpy.props.StringProperty()
|
type_name: bpy.props.StringProperty()
|
||||||
@ -530,6 +612,37 @@ class SessionProps(bpy.types.PropertyGroup):
|
|||||||
is_host: bpy.props.BoolProperty(
|
is_host: bpy.props.BoolProperty(
|
||||||
default=False
|
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 = (
|
classes = (
|
||||||
@ -552,9 +665,20 @@ def register():
|
|||||||
logging.debug('Generating bl_types preferences')
|
logging.debug('Generating bl_types preferences')
|
||||||
prefs.generate_supported_types()
|
prefs.generate_supported_types()
|
||||||
|
|
||||||
|
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'}
|
||||||
|
)
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
from bpy.utils import unregister_class
|
from bpy.utils import unregister_class
|
||||||
|
|
||||||
for cls in reversed(classes):
|
for cls in reversed(classes):
|
||||||
unregister_class(cls)
|
unregister_class(cls)
|
||||||
|
|
||||||
|
del bpy.types.Scene.active_replay_file
|
||||||
|
@ -615,6 +615,39 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel):
|
|||||||
row.active = settings.presence_show_user
|
row.active = settings.presence_show_user
|
||||||
row.prop(settings, "presence_show_far_user")
|
row.prop(settings, "presence_show_far_user")
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
SESSION_UL_users,
|
SESSION_UL_users,
|
||||||
SESSION_PT_settings,
|
SESSION_PT_settings,
|
||||||
@ -624,6 +657,7 @@ classes = (
|
|||||||
SESSION_PT_advanced_settings,
|
SESSION_PT_advanced_settings,
|
||||||
SESSION_PT_user,
|
SESSION_PT_user,
|
||||||
SESSION_PT_repository,
|
SESSION_PT_repository,
|
||||||
|
SESSION_PT_replay,
|
||||||
VIEW3D_PT_overlay_session,
|
VIEW3D_PT_overlay_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -100,18 +100,6 @@ def get_state_str(state):
|
|||||||
return state_str
|
return state_str
|
||||||
|
|
||||||
|
|
||||||
def clean_scene():
|
|
||||||
for type_name in dir(bpy.data):
|
|
||||||
try:
|
|
||||||
type_collection = getattr(bpy.data, type_name)
|
|
||||||
for item in type_collection:
|
|
||||||
type_collection.remove(item)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Clear sequencer
|
|
||||||
bpy.context.scene.sequence_editor_clear()
|
|
||||||
|
|
||||||
def get_selected_objects(scene, active_view_layer):
|
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)]
|
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user