Compare commits

..

27 Commits

Author SHA1 Message Date
630e1c7494 feat: draw xr-user frustum in regular user viewports
Related to https://gitlab.com/slumber/multi-user/-/issues/251
2022-03-27 18:38:38 +02:00
1b614f4fb6 refactor: initial step rewrite user frustum drawing code to debug xr camera position 2022-03-27 17:33:13 +02:00
629f2e1cdb feat: update changelog 2022-03-11 18:52:30 +01:00
b8fed806ed feat: update version 2022-03-11 18:38:38 +01:00
8190846b59 fix: blender 3.1 numpy loading compatibility 2022-03-11 18:38:09 +01:00
c228b6ad7f refactpr: snapshot logs 2022-03-09 11:19:09 +01:00
48651ce890 fix: uuid error when joining a server 2022-03-09 10:42:44 +01:00
26847cf459 fix: server crashing during snapshots 2022-03-08 18:06:54 +01:00
bfa6991c00 fix: server docker file 2022-03-02 14:26:01 +01:00
70b6f9bcfa feat: update changelog for 0.5.2 2022-02-18 11:28:56 +01:00
8d176b55e4 Merge branch '250-geometry-nodes-attribue-toogle-doesn-t-sync' into 'develop'
Resolve "Geometry nodes attribue toogle doesn't sync"

See merge request slumber/multi-user!172
2022-02-17 09:51:41 +00:00
4c0356e724 fix: geometry node boolean parameter loading
related to #250
2022-02-17 10:43:47 +01:00
6b04d1d8d6 Merge branch '248-objects-not-selectable-after-user-leaves-session' into 'develop'
Resolve "objects not selectable after user leaves session"

See merge request slumber/multi-user!171
2022-02-15 10:03:58 +00:00
edfcdd8867 feat: bump version 2022-02-15 11:00:18 +01:00
bdd6599614 fix: objects not selectable after user leaves session (or kicked)
Related to #248
2022-02-15 10:55:26 +01:00
047bd47048 Merge branch '247-auto-updater-breaks-dependency-auto-installer' into 'develop'
feat: bump addon version

See merge request slumber/multi-user!170
2022-02-10 15:20:58 +00:00
d32cbb7b30 feat: bump addon version 2022-02-10 16:20:22 +01:00
adabce3822 Merge branch '247-auto-updater-breaks-dependency-auto-installer' into 'develop'
Resolve "Auto updater breaks dependency auto installer"

See merge request slumber/multi-user!168
2022-02-10 15:10:37 +00:00
62f52db5b2 fix: auto updater with tags 2022-02-10 16:06:53 +01:00
745f45b682 fix: addon directory not cleared during an update 2022-02-10 15:44:46 +01:00
f84860f520 feat: update changelog 2022-02-10 12:06:29 +01:00
c7ee67d4dd fix: replication typo (@kromar) 2022-02-10 11:58:09 +01:00
7ed4644b75 feat: added 0.5.0 update to the changelog 2022-02-10 11:55:14 +01:00
e0c4a17be9 feat: update version 2022-02-10 11:23:46 +01:00
2a6181b832 fix: replication typo 2022-02-10 11:23:01 +01:00
0f7c9adec5 fix: Panel calss prefix warning 2022-02-10 11:20:11 +01:00
f094ec097c doc: remove replication version 2022-02-10 11:15:08 +01:00
13 changed files with 178 additions and 119 deletions

View File

@ -217,3 +217,51 @@ All notable changes to this project will be documented in this file.
- GPencil fill stroke - GPencil fill stroke
- Sculpt and GPencil brushes deleted when joining a session (@Kysios) - Sculpt and GPencil brushes deleted when joining a session (@Kysios)
- Auto-updater doesn't work for master and develop builds - Auto-updater doesn't work for master and develop builds
## [0.5.0] - 2022-02-10
### Added
- New overall UI and UX (@Kysios)
- Documentation overall update (@Kysios)
- Server presets (@Kysios)
- Server online status (@Kysios)
- Draw connected user color in the user list
- Private session (access protected with a password) (@Kysios)
### Changed
- Dependencies are now installed in the addon folder and correctly cleaned during the addon removal process
### Fixed
- Python 3.10 compatibility (@notfood)
- Blender 3.x compatibility
- Skin vertex radius synchronization (@kromar)
- Sequencer audio strip synchronization
- Crash with empty after a reconnection
## [0.5.1] - 2022-02-10
### Fixed
- Auto updater breaks dependency auto installer
- Auto updater update from tag
## [0.5.2] - 2022-02-18
### Fixed
- Objects not selectable after user leaves session
- Geometry nodes attribute toogle doesn't sync
## [0.5.3] - 2022-03-11
### Changed
- Snapshots logs
### Fixed
- Server crashing during snapshots
- Blender 3.1 numpy loading error during early connection process
- Server docker arguments

View File

@ -206,9 +206,9 @@ You can run the dedicated server on any platform by following these steps:
.. code-block:: bash .. code-block:: bash
python -m pip install replication==0.1.13 python -m pip install replication
4. Launch the server with: 3. Launch the server with:
.. code-block:: bash .. code-block:: bash
@ -562,7 +562,7 @@ The default Docker image essentially runs the equivalent of:
.. code-block:: bash .. code-block:: bash
replication.serve -pwd admin -p 5555 -t 5000 -l DEBUG -lf multiuser_server.log replication.server -pwd admin -p 5555 -t 5000 -l DEBUG -lf multiuser_server.log
This means the server will be launched with 'admin' as the administrator password, run on ports 5555:5558, use a timeout of 5 seconds, verbose 'DEBUG' log level, and with log files written to 'multiuser_server.log'. See :ref:`cmd-line` for a description of optional parameters. This means the server will be launched with 'admin' as the administrator password, run on ports 5555:5558, use a timeout of 5 seconds, verbose 'DEBUG' log level, and with log files written to 'multiuser_server.log'. See :ref:`cmd-line` for a description of optional parameters.

View File

@ -19,7 +19,7 @@
bl_info = { bl_info = {
"name": "Multi-User", "name": "Multi-User",
"author": "Swann Martinez", "author": "Swann Martinez",
"version": (0, 4, 1), "version": (0, 5, 4),
"description": "Enable real-time collaborative workflow inside blender", "description": "Enable real-time collaborative workflow inside blender",
"blender": (2, 82, 0), "blender": (2, 82, 0),
"location": "3D View > Sidebar > Multi-User tab", "location": "3D View > Sidebar > Multi-User tab",

View File

@ -1015,16 +1015,18 @@ class Singleton_updater(object):
for path, dirs, files in os.walk(base): for path, dirs, files in os.walk(base):
# prune ie skip updater folder # prune ie skip updater folder
dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]] dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]]
for directory in dirs:
shutil.rmtree(os.path.join(path,directory))
for file in files: for file in files:
for ptrn in self.remove_pre_update_patterns: try:
if fnmatch.filter([file],ptrn): fl = os.path.join(path,file)
try: os.remove(fl)
fl = os.path.join(path,file) if self._verbose: print("Pre-removed file "+file)
os.remove(fl) except OSError:
if self._verbose: print("Pre-removed file "+file) print("Failed to pre-remove "+file)
except OSError: self.print_trace()
print("Failed to pre-remove "+file)
self.print_trace()
# Walk through the temp addon sub folder for replacements # Walk through the temp addon sub folder for replacements
# this implements the overwrite rules, which apply after # this implements the overwrite rules, which apply after
@ -1701,7 +1703,7 @@ class GitlabEngine(object):
def parse_tags(self, response, updater): def parse_tags(self, response, updater):
if response == None: if response == None:
return [] return []
return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["commit"]["id"], updater)} for tag in response] return [{"name": tag["name"], "zipball_url": f"https://gitlab.com/slumber/multi-user/-/jobs/artifacts/{tag['name']}/download?job=build"} for tag in response]
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@ -267,7 +267,7 @@ class addon_updater_update_now(bpy.types.Operator):
clean_install: bpy.props.BoolProperty( clean_install: bpy.props.BoolProperty(
name="Clean install", name="Clean install",
description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install", description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
default=False, default=True,
options={'HIDDEN'} options={'HIDDEN'}
) )

View File

@ -47,10 +47,7 @@ SHAPEKEY_BLOCK_ATTR = [
'slider_max', 'slider_max',
] ]
CURVE_POINT = [
'location',
'handle_type_2',
]
if bpy.app.version >= (2,93,0): if bpy.app.version >= (2,93,0):
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float) SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float)
else: else:
@ -175,7 +172,7 @@ def load_modifier_geometry_node_props(dumped_modifier: dict, target_modifier: bp
for input_index, inpt in enumerate(get_node_group_properties_identifiers(target_modifier.node_group)): for input_index, inpt in enumerate(get_node_group_properties_identifiers(target_modifier.node_group)):
dumped_value, dumped_type = dumped_modifier['props'][input_index] dumped_value, dumped_type = dumped_modifier['props'][input_index]
input_value = target_modifier[inpt[0]] input_value = target_modifier[inpt[0]]
if dumped_type in ['INT', 'VALUE', 'STR']: if dumped_type in ['INT', 'VALUE', 'STR', 'BOOL']:
logging.info(f"{inpt[0]}/{dumped_value}") logging.info(f"{inpt[0]}/{dumped_value}")
target_modifier[inpt[0]] = dumped_value target_modifier[inpt[0]] = dumped_value
elif dumped_type in ['RGBA', 'VECTOR']: elif dumped_type in ['RGBA', 'VECTOR']:
@ -421,8 +418,7 @@ def dump_modifiers(modifiers: bpy.types.bpy_prop_collection)->dict:
dumped_modifier['settings'] = dumper.dump(modifier.settings) dumped_modifier['settings'] = dumper.dump(modifier.settings)
elif modifier.type == 'UV_PROJECT': elif modifier.type == 'UV_PROJECT':
dumped_modifier['projectors'] =[p.object.name for p in modifier.projectors if p and p.object] dumped_modifier['projectors'] =[p.object.name for p in modifier.projectors if p and p.object]
elif modifier.type == 'BEVEL' and modifier.profile_type == 'CUSTOM':
dumped_modifier['custom_profile'] = np_dump_collection(modifier.custom_profile.points, CURVE_POINT)
dumped_modifiers.append(dumped_modifier) dumped_modifiers.append(dumped_modifier)
return dumped_modifiers return dumped_modifiers
@ -494,31 +490,11 @@ def load_modifiers(dumped_modifiers: list, modifiers: bpy.types.bpy_prop_collect
loaded_modifier.projectors[projector_index].object = target_object loaded_modifier.projectors[projector_index].object = target_object
else: else:
logging.error("Could't load projector target object {projector_object}") logging.error("Could't load projector target object {projector_object}")
elif loaded_modifier.type == 'BEVEL':
src_cust_profile = dumped_modifier.get('custom_profile')
if src_cust_profile:
dst_points = loaded_modifier.custom_profile.points
# TODO: refactor to be diff-compatible
for p in dst_points:
try:
dst_points.remove(dst_points[0])
except Exception:
break
for i in range(len(src_cust_profile['handle_type_2'])-len(dst_points)):
dst_points.add(0,0)
np_load_collection(src_cust_profile, dst_points, CURVE_POINT)
loaded_modifier.custom_profile.points.update()
def load_modifiers_custom_data(dumped_modifiers: dict, modifiers: bpy.types.bpy_prop_collection): def load_modifiers_custom_data(dumped_modifiers: dict, modifiers: bpy.types.bpy_prop_collection):
""" Load modifiers custom data not managed by the dump_anything loader """ Load modifiers custom data not managed by the dump_anything loader
git
:param dumped_modifiers: modifiers to load :param dumped_modifiers: modifiers to load
:type dumped_modifiers: dict :type dumped_modifiers: dict
:param modifiers: target modifiers collection :param modifiers: target modifiers collection

View File

@ -26,7 +26,8 @@ import numpy as np
BPY_TO_NUMPY_TYPES = { BPY_TO_NUMPY_TYPES = {
'FLOAT': np.float32, 'FLOAT': np.float32,
'INT': np.int32, 'INT': np.int32,
'BOOL': np.bool} 'BOOL': np.bool,
'BOOLEAN': np.bool}
PRIMITIVE_TYPES = ['FLOAT', 'INT', 'BOOLEAN'] PRIMITIVE_TYPES = ['FLOAT', 'INT', 'BOOLEAN']
@ -581,7 +582,6 @@ class Loader:
else: else:
dst_curve.points.new(pos[0], pos[1]) dst_curve.points.new(pos[0], pos[1])
curves.update() curves.update()
def _load_pointer(self, instance, dump): def _load_pointer(self, instance, dump):
rna_property_type = instance.bl_rna_property.fixed_type rna_property_type = instance.bl_rna_property.fixed_type

View File

@ -23,6 +23,7 @@ from replication import porcelain
from replication.constants import RP_COMMON, STATE_ACTIVE, STATE_SYNCING, UP from replication.constants import RP_COMMON, STATE_ACTIVE, STATE_SYNCING, UP
from replication.exception import ContextError, NonAuthorizedOperationError from replication.exception import ContextError, NonAuthorizedOperationError
from replication.interface import session from replication.interface import session
from .timers import XrUserUpdate
from . import shared_data, utils from . import shared_data, utils
@ -46,14 +47,16 @@ def sanitize_deps_graph(remove_nodes: bool = False):
rm_cpt += 1 rm_cpt += 1
except NonAuthorizedOperationError: except NonAuthorizedOperationError:
continue continue
logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes") logging.info(
f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes")
def update_external_dependencies(): def update_external_dependencies():
"""Force external dependencies(files such as images) evaluation """Force external dependencies(files such as images) evaluation
""" """
external_types = ['WindowsPath', 'PosixPath', 'Image'] external_types = ['WindowsPath', 'PosixPath', 'Image']
nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in external_types] nodes_ids = [n.uuid for n in session.repository.graph.values()
if n.data['type_id'] in external_types]
for node_id in nodes_ids: for node_id in nodes_ids:
node = session.repository.graph.get(node_id) node = session.repository.graph.get(node_id)
if node and node.owner in [session.repository.username, RP_COMMON]: if node and node.owner in [session.repository.username, RP_COMMON]:
@ -72,11 +75,13 @@ def on_scene_update(scene):
settings = utils.get_preferences() settings = utils.get_preferences()
incoming_updates = shared_data.session.applied_updates incoming_updates = shared_data.session.applied_updates
distant_update = [getattr(u.id, 'uuid', None) for u in dependency_updates if getattr(u.id, 'uuid', None) in incoming_updates] distant_update = [getattr(u.id, 'uuid', None) for u in dependency_updates if getattr(
u.id, 'uuid', None) in incoming_updates]
if distant_update: if distant_update:
for u in distant_update: for u in distant_update:
shared_data.session.applied_updates.remove(u) shared_data.session.applied_updates.remove(u)
logging.debug(f"Ignoring distant update of {dependency_updates[0].id.name}") logging.debug(
f"Ignoring distant update of {dependency_updates[0].id.name}")
return return
# NOTE: maybe we don't need to check each update but only the first # NOTE: maybe we don't need to check each update but only the first
@ -84,7 +89,8 @@ def on_scene_update(scene):
update_uuid = getattr(update.id, 'uuid', None) update_uuid = getattr(update.id, 'uuid', None)
if update_uuid: if update_uuid:
node = session.repository.graph.get(update.id.uuid) node = session.repository.graph.get(update.id.uuid)
check_common = session.repository.rdp.get_implementation(update.id).bl_check_common check_common = session.repository.rdp.get_implementation(
update.id).bl_check_common
if node and (node.owner == session.repository.username or check_common): if node and (node.owner == session.repository.username or check_common):
logging.debug(f"Evaluate {update.id.name}") logging.debug(f"Evaluate {update.id.name}")
@ -107,12 +113,14 @@ def on_scene_update(scene):
porcelain.commit(session.repository, scn_uuid) porcelain.commit(session.repository, scn_uuid)
porcelain.push(session.repository, 'origin', scn_uuid) porcelain.push(session.repository, 'origin', scn_uuid)
scene_graph_changed = [u for u in reversed(dependency_updates) if getattr(u.id, 'uuid', None) and isinstance(u.id,(bpy.types.Scene,bpy.types.Collection))] scene_graph_changed = [u for u in reversed(dependency_updates) if getattr(
u.id, 'uuid', None) and isinstance(u.id, (bpy.types.Scene, bpy.types.Collection))]
if scene_graph_changed: if scene_graph_changed:
porcelain.purge_orphan_nodes(session.repository) porcelain.purge_orphan_nodes(session.repository)
update_external_dependencies() update_external_dependencies()
@persistent @persistent
def resolve_deps_graph(dummy): def resolve_deps_graph(dummy):
"""Resolve deps graph """Resolve deps graph
@ -138,6 +146,13 @@ def update_client_frame(scene):
'frame_current': scene.frame_current 'frame_current': scene.frame_current
}) })
@persistent
def xr_user_update(scene):
if session and session.state == STATE_ACTIVE:
xr_timer = XrUserUpdate()
xr_timer.register()
logging.info("XR Session timer started")
def register(): def register():
bpy.app.handlers.undo_post.append(resolve_deps_graph) bpy.app.handlers.undo_post.append(resolve_deps_graph)
@ -145,6 +160,8 @@ def register():
bpy.app.handlers.load_pre.append(load_pre_handler) bpy.app.handlers.load_pre.append(load_pre_handler)
bpy.app.handlers.frame_change_pre.append(update_client_frame) bpy.app.handlers.frame_change_pre.append(update_client_frame)
bpy.app.handlers.xr_session_start_pre.append(xr_user_update)
def unregister(): def unregister():
@ -153,3 +170,5 @@ def unregister():
bpy.app.handlers.load_pre.remove(load_pre_handler) bpy.app.handlers.load_pre.remove(load_pre_handler)
bpy.app.handlers.frame_change_pre.remove(update_client_frame) bpy.app.handlers.frame_change_pre.remove(update_client_frame)
bpy.app.handlers.xr_session_start_pre.remove(xr_user_update)

View File

@ -68,6 +68,7 @@ stop_modal_executor = False
def draw_user(username, metadata, radius=0.01, intensity=10.0): def draw_user(username, metadata, radius=0.01, intensity=10.0):
# TODO: Draw camera model from viewmatrix
view_corners = metadata.get('view_corners') view_corners = metadata.get('view_corners')
color = metadata.get('color', (1,1,1,0)) color = metadata.get('color', (1,1,1,0))
objects = metadata.get('selected_objects', None) objects = metadata.get('selected_objects', None)
@ -1036,7 +1037,7 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
def poll(cls, context): def poll(cls, context):
return True return True
class SessionImportUser(bpy.types.Panel): class SESSION_PT_ImportUser(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER' bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS' bl_region_type = 'TOOL_PROPS'
bl_label = "Users" bl_label = "Users"
@ -1300,7 +1301,7 @@ classes = (
SessionNotifyOperator, SessionNotifyOperator,
SessionSaveBackupOperator, SessionSaveBackupOperator,
SessionLoadSaveOperator, SessionLoadSaveOperator,
SessionImportUser, SESSION_PT_ImportUser,
SessionStopAutoSaveOperator, SessionStopAutoSaveOperator,
SessionPurgeOperator, SessionPurgeOperator,
SessionPresetServerAdd, SessionPresetServerAdd,

View File

@ -26,7 +26,7 @@ import bgl
import blf import blf
import bpy import bpy
import gpu import gpu
import mathutils from mathutils import Vector, Matrix, Quaternion
from bpy_extras import view3d_utils from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from replication.constants import (STATE_ACTIVE, STATE_AUTH, STATE_CONFIG, from replication.constants import (STATE_ACTIVE, STATE_AUTH, STATE_CONFIG,
@ -136,7 +136,7 @@ def bbox_from_obj(obj: bpy.types.Object, index: int = 1) -> list:
(-radius, +radius, +radius), (+radius, +radius, +radius)] (-radius, +radius, +radius), (+radius, +radius, +radius)]
base = obj.matrix_world base = obj.matrix_world
bbox_corners = [base @ mathutils.Vector(corner) for corner in coords] bbox_corners = [base @ Vector(corner) for corner in coords]
vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners] vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners]
@ -159,39 +159,12 @@ def bbox_from_instance_collection(ic: bpy.types.Object, index: int = 0) -> list:
vertex_pos += vertex_pos_temp vertex_pos += vertex_pos_temp
vertex_indices += vertex_indices_temp vertex_indices += vertex_indices_temp
bbox_corners = [ic.matrix_world @ mathutils.Vector(vertex) for vertex in vertex_pos] bbox_corners = [ic.matrix_world @ Vector(vertex) for vertex in vertex_pos]
vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners] vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners]
return vertex_pos, vertex_indices return vertex_pos, vertex_indices
def generate_user_camera() -> list:
""" Generate a basic camera represention of the user point of view
:return: list of 7 points
"""
area, region, rv3d = view3d_find()
v1 = v2 = v3 = v4 = v5 = v6 = v7 = [0, 0, 0]
if area and region and rv3d:
width = region.width
height = region.height
v1 = project_to_viewport(region, rv3d, (0, 0))
v3 = project_to_viewport(region, rv3d, (0, height))
v2 = project_to_viewport(region, rv3d, (width, height))
v4 = project_to_viewport(region, rv3d, (width, 0))
v5 = project_to_viewport(region, rv3d, (width/2, height/2))
v6 = list(rv3d.view_location)
v7 = project_to_viewport(
region, rv3d, (width/2, height/2), distance=-.8)
coords = [v1, v2, v3, v4, v5, v6, v7]
return coords
def project_to_screen(coords: list) -> list: def project_to_screen(coords: list) -> list:
""" Project 3D coordinate to 2D screen coordinates """ Project 3D coordinate to 2D screen coordinates
@ -219,10 +192,10 @@ def get_bb_coords_from_obj(object: bpy.types.Object, instance: bpy.types.Object
base = object.matrix_world base = object.matrix_world
if instance: if instance:
scale = mathutils.Matrix.Diagonal(object.matrix_world.to_scale()) scale = Matrix.Diagonal(object.matrix_world.to_scale())
base = instance.matrix_world @ scale.to_4x4() base = instance.matrix_world @ scale.to_4x4()
bbox_corners = [base @ mathutils.Vector( bbox_corners = [base @ Vector(
corner) for corner in object.bound_box] corner) for corner in object.bound_box]
@ -267,9 +240,14 @@ class Widget(object):
class UserFrustumWidget(Widget): class UserFrustumWidget(Widget):
# Camera widget indices # Camera widget indices
indices = ((1, 3), (2, 1), (3, 0), camera_vertex = ((0, 0, 1),
(2, 0), (4, 5), (1, 6), (-1, -0.5, -1), (1, -0.5, -1), (1, 0.5, -1), (-1, 0.5, -1),
(2, 6), (3, 6), (0, 6)) (0, 1, -1),
(-0.5, 0.6, -1), (0.5, 0.6, -1))
camera_indices = ((0, 1), (0, 2), (0, 3), (0, 4),
(1, 2), (2, 3), (3, 4), (4, 1),
(5, 6), (6, 7), (7, 5))
def __init__( def __init__(
self, self,
@ -290,27 +268,33 @@ class UserFrustumWidget(Widget):
return False return False
scene_current = self.data.get('scene_current') scene_current = self.data.get('scene_current')
view_corners = self.data.get('view_corners') view_matrix = self.data.get('view_matrix')
return (scene_current == bpy.context.scene.name or return (scene_current == bpy.context.scene.name or
self.settings.presence_show_far_user) and \ self.settings.presence_show_far_user) and \
view_corners and \ view_matrix and \
self.settings.presence_show_user and \ self.settings.presence_show_user and \
self.settings.enable_presence self.settings.enable_presence
def draw(self): def draw(self):
location = self.data.get('view_corners')
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR') shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
positions = [tuple(coord) for coord in location] xr_state = self.data.get('xr')
transformation = Matrix()
if len(positions) != 7: if xr_state:
return loc = Vector(xr_state.get('viewer_pose_location'))
rot = Quaternion(xr_state.get('viewer_pose_rotation'))
scale = Vector((1,1,1))
transformation = Matrix.LocRotScale(loc, rot, scale)
else:
transformation = Matrix(self.data.get('view_matrix')).inverted()
coords = [transformation @ Vector(vertex) for vertex in self.camera_vertex]
batch = batch_for_shader( batch = batch_for_shader(
shader, shader,
'LINES', 'LINES',
{"pos": positions}, {"pos": coords},
indices=self.indices) indices=self.camera_indices)
shader.bind() shader.bind()
shader.uniform_float("color", self.data.get('color')) shader.uniform_float("color", self.data.get('color'))
@ -405,19 +389,24 @@ class UserNameWidget(Widget):
return False return False
scene_current = self.data.get('scene_current') scene_current = self.data.get('scene_current')
view_corners = self.data.get('view_corners') view_matrix = self.data.get('view_matrix')
return (scene_current == bpy.context.scene.name or return (scene_current == bpy.context.scene.name or
self.settings.presence_show_far_user) and \ self.settings.presence_show_far_user) and \
view_corners and \ view_matrix and \
self.settings.presence_show_user and \ self.settings.presence_show_user and \
self.settings.enable_presence self.settings.enable_presence
def draw(self): def draw(self):
view_corners = self.data.get('view_corners') xr_state = self.data.get('xr')
if xr_state:
position = xr_state.get('viewer_pose_location', [0,0,0])
else:
position = Matrix(self.data.get('view_matrix')).inverted().to_translation()
color = self.data.get('color') color = self.data.get('color')
position = [tuple(coord) for coord in view_corners] coords = project_to_screen(position)
coords = project_to_screen(position[1])
if coords: if coords:
blf.position(0, coords[0], coords[1]+10, 0) blf.position(0, coords[0], coords[1]+10, 0)

View File

@ -28,8 +28,7 @@ from replication import porcelain
from . import operators, utils from . import operators, utils
from .presence import (UserFrustumWidget, UserNameWidget, UserModeWidget, UserSelectionWidget, from .presence import (UserFrustumWidget, UserNameWidget, UserModeWidget, UserSelectionWidget,
generate_user_camera, get_view_matrix, refresh_3d_view, get_view_matrix, refresh_3d_view, refresh_sidebar_view, renderer)
refresh_sidebar_view, renderer)
from . import shared_data from . import shared_data
@ -276,21 +275,24 @@ class ClientUpdate(Timer):
if session and renderer: if session and renderer:
if session.state in [STATE_ACTIVE, STATE_LOBBY]: if session.state in [STATE_ACTIVE, STATE_LOBBY]:
local_user = session.online_users.get( local_user = session.online_users.get(settings.username)
settings.username) xr_session_state = bpy.context.window_manager.xr_session_state
if not local_user: if not local_user:
return return
else: else:
for username, user_data in session.online_users.items(): for username, user_data in session.online_users.items():
if username != settings.username: if username != settings.username:
cached_user_data = self.users_metadata.get( cached_user_data = self.users_metadata.get(username)
username)
new_user_data = session.online_users[username]['metadata'] new_user_data = session.online_users[username]['metadata']
if cached_user_data is None: if cached_user_data is None:
self.users_metadata[username] = user_data['metadata'] self.users_metadata[username] = user_data['metadata']
elif 'view_matrix' in cached_user_data and 'view_matrix' in new_user_data and cached_user_data['view_matrix'] != new_user_data['view_matrix']: elif 'view_matrix' in cached_user_data and \
'view_matrix' in new_user_data and \
cached_user_data['view_matrix'] != new_user_data['view_matrix'] or \
'xr' in cached_user_data and \
'xr' in new_user_data and \
cached_user_data['xr']['viewer_pose_location'] != new_user_data['xr']['viewer_pose_location']:
refresh_3d_view() refresh_3d_view()
self.users_metadata[username] = user_data['metadata'] self.users_metadata[username] = user_data['metadata']
break break
@ -300,13 +302,12 @@ class ClientUpdate(Timer):
local_user_metadata = local_user.get('metadata') local_user_metadata = local_user.get('metadata')
scene_current = bpy.context.scene.name scene_current = bpy.context.scene.name
local_user = session.online_users.get(settings.username) local_user = session.online_users.get(settings.username)
current_view_corners = generate_user_camera() current_view_matrix = get_view_matrix()
# Init client metadata # Init client metadata
if not local_user_metadata or 'color' not in local_user_metadata.keys(): if not local_user_metadata or 'color' not in local_user_metadata.keys():
metadata = { metadata = {
'view_corners': get_view_matrix(), 'view_matrix': current_view_matrix,
'view_matrix': get_view_matrix(),
'color': (settings.client_color.r, 'color': (settings.client_color.r,
settings.client_color.g, settings.client_color.g,
settings.client_color.b, settings.client_color.b,
@ -322,10 +323,8 @@ class ClientUpdate(Timer):
elif scene_current != local_user_metadata['scene_current']: elif scene_current != local_user_metadata['scene_current']:
local_user_metadata['scene_current'] = scene_current local_user_metadata['scene_current'] = scene_current
porcelain.update_user_metadata(session.repository, local_user_metadata) porcelain.update_user_metadata(session.repository, local_user_metadata)
elif 'view_corners' in local_user_metadata and current_view_corners != local_user_metadata['view_corners']: elif 'view_matrix' in local_user_metadata and current_view_matrix != local_user_metadata['view_matrix']:
local_user_metadata['view_corners'] = current_view_corners local_user_metadata['view_matrix'] = current_view_matrix
local_user_metadata['view_matrix'] = get_view_matrix(
)
porcelain.update_user_metadata(session.repository, local_user_metadata) porcelain.update_user_metadata(session.repository, local_user_metadata)
elif bpy.context.mode != local_user_metadata['mode_current']: elif bpy.context.mode != local_user_metadata['mode_current']:
local_user_metadata['mode_current'] = bpy.context.mode local_user_metadata['mode_current'] = bpy.context.mode
@ -387,3 +386,28 @@ class MainThreadExecutor(Timer):
function, kwargs = self.execution_queue.get() function, kwargs = self.execution_queue.get()
logging.debug(f"Executing {function.__name__}") logging.debug(f"Executing {function.__name__}")
function(**kwargs) function(**kwargs)
class XrUserUpdate(Timer):
def __init__(self, timeout=.01):
# TODO: Add user refresh rate settings
super().__init__(timeout)
def execute(self):
xr_session_state = bpy.context.window_manager.xr_session_state
if xr_session_state and xr_session_state.is_running:
# Update user state
porcelain.update_user_metadata(
session.repository,
{'xr': {
'viewer_pose_location': list(xr_session_state.viewer_pose_location),
'viewer_pose_rotation': list(xr_session_state.viewer_pose_rotation),
'controller_0_location': list(xr_session_state.controller_grip_location_get(bpy.context, 0)),
'controller_0_rotation': list(xr_session_state.controller_grip_rotation_get(bpy.context, 0)),
'controller_1_location': list(xr_session_state.controller_grip_location_get(bpy.context, 1)),
'controller_1_rotation': list(xr_session_state.controller_grip_rotation_get(bpy.context, 1))}
})
else:
logging.info("XR Session ended, stopping user update")
self.unregister()

View File

@ -22,4 +22,4 @@ RUN pip install replication==$replication_version
# Run the server with parameters # Run the server with parameters
ENTRYPOINT ["/bin/sh", "-c"] ENTRYPOINT ["/bin/sh", "-c"]
CMD ["replication.serve -apwd ${password} -spwd '' -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"] CMD ["replication.server -apwd ${password} -spwd '' -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"]