Compare commits

..

6 Commits

20 changed files with 219 additions and 1015 deletions
-1
View File
@@ -5,7 +5,6 @@ stages:
- doc - doc
include: include:
- local: .gitlab/ci/test.gitlab-ci.yml - local: .gitlab/ci/test.gitlab-ci.yml
- local: .gitlab/ci/build.gitlab-ci.yml - local: .gitlab/ci/build.gitlab-ci.yml
+5 -1
View File
@@ -1,6 +1,5 @@
build: build:
stage: build stage: build
needs: ["test"]
image: debian:stable-slim image: debian:stable-slim
script: script:
- rm -rf tests .git .gitignore script - rm -rf tests .git .gitignore script
@@ -8,3 +7,8 @@ build:
name: multi_user name: multi_user
paths: paths:
- multi_user - multi_user
only:
refs:
- master
+4 -1
View File
@@ -1,6 +1,5 @@
deploy: deploy:
stage: deploy stage: deploy
needs: ["build"]
image: slumber/docker-python image: slumber/docker-python
variables: variables:
DOCKER_DRIVER: overlay2 DOCKER_DRIVER: overlay2
@@ -18,3 +17,7 @@ deploy:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker tag registry.gitlab.com/slumber/multi-user/multi-user-server:${VERSION} registry.gitlab.com/slumber/multi-user/multi-user-server:${CI_COMMIT_REF_NAME} - docker tag registry.gitlab.com/slumber/multi-user/multi-user-server:${VERSION} registry.gitlab.com/slumber/multi-user/multi-user-server:${CI_COMMIT_REF_NAME}
- docker push registry.gitlab.com/slumber/multi-user/multi-user-server - docker push registry.gitlab.com/slumber/multi-user/multi-user-server
only:
refs:
- master
+2 -7
View File
@@ -1,16 +1,11 @@
pages: pages:
stage: doc stage: doc
needs: ["deploy"] image: python:3.7-alpine
image: python
script: script:
- pip install -U sphinx sphinx_rtd_theme sphinx-material - pip install -U sphinx sphinx_rtd_theme
- sphinx-build -b html ./docs public - sphinx-build -b html ./docs public
artifacts: artifacts:
paths: paths:
- public - public
only:
refs:
- master
- develop
+3
View File
@@ -3,3 +3,6 @@ test:
image: slumber/blender-addon-testing:latest image: slumber/blender-addon-testing:latest
script: script:
- python3 scripts/test_addon.py - python3 scripts/test_addon.py
only:
refs:
- master
-29
View File
@@ -158,32 +158,3 @@ All notable changes to this project will be documented in this file.
- Material renaming - Material renaming
- Default material nodes input parameters - Default material nodes input parameters
- blender 2.91 python api compatibility - blender 2.91 python api compatibility
## [0.3.0] - 2021-04-14
### Added
- Curve material support
- Cycle visibility settings
- Session save/load operator
- Add new scene support
- Physic initial support
- Geometry node initial support
- Blender 2.93 compatibility
### Changed
- Host documentation on Gitlab Page
- Event driven update (from the blender deps graph)
### Fixed
- Vertex group assignation
- Parent relation can't be removed
- Separate object
- Delete animation
- Sync missing holdout option for grease pencil material
- Sync missing `skin_vertices`
- Exception access violation during Undo/Redo
- Sync missing armature bone Roll
- Sync missing driver data_path
- Constraint replication
+31 -33
View File
@@ -19,46 +19,44 @@ This tool aims to allow multiple users to work on the same scene over the networ
## Usage ## Usage
See the [documentation](https://slumber.gitlab.io/multi-user/index.html) for details. See the [documentation](https://multi-user.readthedocs.io/en/latest/) for details.
## Troubleshooting ## Troubleshooting
See the [troubleshooting guide](https://slumber.gitlab.io/multi-user/getting_started/troubleshooting.html) for tips on the most common issues. See the [troubleshooting guide](https://multi-user.readthedocs.io/en/latest/getting_started/troubleshooting.html) for tips on the most common issues.
## Current development status ## Current development status
Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones. Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones.
| Name | Status | Comment | | Name | Status | Comment |
| -------------- | :----: | :----------------------------------------------------------: | | ----------- | :----: | :--------------------------------------------------------------------------: |
| action | ✔️ | | | action | ✔️ | |
| armature | ❗ | Not stable | | armature | ❗ | Not stable |
| camera | ✔️ | | | camera | ✔️ | |
| collection | ✔️ | | | collection | ✔️ | |
| curve | ❗ | Nurbs surfaces not supported | | curve | ❗ | Nurbs not supported |
| gpencil | ✔️ | | | gpencil | ✔️ | [Airbrush not supported](https://gitlab.com/slumber/multi-user/-/issues/123) |
| image | ✔️ | | | image | ✔️ | |
| mesh | ✔️ | | | mesh | ✔️ | |
| material | ✔️ | | | material | ✔️ | |
| node_groups | ❗ | Material & Geometry only | | node_groups | ❗ | Material only |
| geometry nodes | ✔️ | | | metaball | ✔️ | |
| metaball | ✔️ | | | object | ✔️ | |
| object | ✔️ | | | textures | ❗ | Supported for modifiers only |
| textures | ❗ | Supported for modifiers/materials/geo nodes only | | texts | ✔️ | |
| texts | ✔️ | | | scene | ✔️ | |
| scene | ✔️ | | | world | ✔️ | |
| world | ✔️ | | | lightprobes | ✔️ | |
| lightprobes | ✔️ | | | compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) | | texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) | | nla | ❌ | |
| nla | ❌ | | | volumes | ✔️ | |
| volumes | ✔️ | | | particles | ❌ | [On-going](https://gitlab.com/slumber/multi-user/-/issues/24) |
| particles | ❗ | The cache isn't syncing. | | speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) |
| speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) | | vse | ❗ | Mask and Clip not supported yet |
| vse | ❗ | Mask and Clip not supported yet | | physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) | | libraries | ❗ | Partial |
| libraries | ❗ | Partial |
### Performance issues ### Performance issues
@@ -76,7 +74,7 @@ I'm working on it.
## Contributing ## Contributing
See [contributing section](https://slumber.gitlab.io/multi-user/ways_to_contribute.html) of the documentation. See [contributing section](https://multi-user.readthedocs.io/en/latest/ways_to_contribute.html) of the documentation.
Feel free to [join the discord server](https://discord.gg/aBPvGws) to chat, seek help and contribute. Feel free to [join the discord server](https://discord.gg/aBPvGws) to chat, seek help and contribute.
+7 -7
View File
@@ -122,13 +122,13 @@ class addon_updater_install_popup(bpy.types.Operator):
# if true, run clean install - ie remove all files before adding new # if true, run clean install - ie remove all files before adding new
# equivalent to deleting the addon and reinstalling, except the # equivalent to deleting the addon and reinstalling, except the
# updater folder/backup folder remains # updater folder/backup folder remains
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=False,
options={'HIDDEN'} options={'HIDDEN'}
) )
ignore_enum: bpy.props.EnumProperty( ignore_enum = bpy.props.EnumProperty(
name="Process update", name="Process update",
description="Decide to install, ignore, or defer new addon update", description="Decide to install, ignore, or defer new addon update",
items=[ items=[
@@ -264,7 +264,7 @@ class addon_updater_update_now(bpy.types.Operator):
# if true, run clean install - ie remove all files before adding new # if true, run clean install - ie remove all files before adding new
# equivalent to deleting the addon and reinstalling, except the # equivalent to deleting the addon and reinstalling, except the
# updater folder/backup folder remains # updater folder/backup folder remains
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=False,
@@ -332,7 +332,7 @@ class addon_updater_update_target(bpy.types.Operator):
i+=1 i+=1
return ret return ret
target: bpy.props.EnumProperty( target = bpy.props.EnumProperty(
name="Target version to install", name="Target version to install",
description="Select the version to install", description="Select the version to install",
items=target_version items=target_version
@@ -341,7 +341,7 @@ class addon_updater_update_target(bpy.types.Operator):
# if true, run clean install - ie remove all files before adding new # if true, run clean install - ie remove all files before adding new
# equivalent to deleting the addon and reinstalling, except the # equivalent to deleting the addon and reinstalling, except the
# updater folder/backup folder remains # updater folder/backup folder remains
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=False,
@@ -399,7 +399,7 @@ class addon_updater_install_manually(bpy.types.Operator):
bl_description = "Proceed to manually install update" bl_description = "Proceed to manually install update"
bl_options = {'REGISTER', 'INTERNAL'} bl_options = {'REGISTER', 'INTERNAL'}
error: bpy.props.StringProperty( error = bpy.props.StringProperty(
name="Error Occurred", name="Error Occurred",
default="", default="",
options={'HIDDEN'} options={'HIDDEN'}
@@ -461,7 +461,7 @@ class addon_updater_updated_successful(bpy.types.Operator):
bl_description = "Update installation response" bl_description = "Update installation response"
bl_options = {'REGISTER', 'INTERNAL', 'UNDO'} bl_options = {'REGISTER', 'INTERNAL', 'UNDO'}
error: bpy.props.StringProperty( error = bpy.props.StringProperty(
name="Error Occurred", name="Error Occurred",
default="", default="",
options={'HIDDEN'} options={'HIDDEN'}
-1
View File
@@ -42,7 +42,6 @@ __all__ = [
# 'bl_sequencer', # 'bl_sequencer',
'bl_node_group', 'bl_node_group',
'bl_texture', 'bl_texture',
"bl_particle",
] # Order here defines execution order ] # Order here defines execution order
if bpy.app.version[1] >= 91: if bpy.app.version[1] >= 91:
+102 -129
View File
@@ -27,7 +27,7 @@ from .dump_anything import Loader, Dumper
from .bl_datablock import BlDatablock, get_datablock_from_uuid from .bl_datablock import BlDatablock, get_datablock_from_uuid
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]') NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
IGNORED_SOCKETS = ['GEOMETRY', 'SHADER', 'CUSTOM'] IGNORED_SOCKETS = ['GEOMETRY']
def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree): def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
""" Load a node into a node_tree from a dict """ Load a node into a node_tree from a dict
@@ -53,136 +53,31 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
inputs_data = node_data.get('inputs') inputs_data = node_data.get('inputs')
if inputs_data: if inputs_data:
inputs = [i for i in target_node.inputs if i.type not in IGNORED_SOCKETS] inputs = [i for i in target_node.inputs if i.type not in IGNORED_SOCKETS]
for idx, inpt in enumerate(inputs):
if idx < len(inputs_data) and hasattr(inpt, "default_value"): for idx, inpt in enumerate(inputs_data):
loaded_input = inputs_data[idx] if idx < len(inputs) and hasattr(inputs[idx], "default_value"):
try: try:
if inpt.type in ['OBJECT', 'COLLECTION']: inputs[idx].default_value = inpt
inpt.default_value = get_datablock_from_uuid(loaded_input, None)
else:
inpt.default_value = loaded_input
except Exception as e: except Exception as e:
logging.warning(f"Node {target_node.name} input {inpt.name} parameter not supported, skipping ({e})") logging.warning(f"Node {target_node.name} input {inputs[idx].name} parameter not supported, skipping ({e})")
else: else:
logging.warning(f"Node {target_node.name} input length mismatch.") logging.warning(f"Node {target_node.name} input length mismatch.")
outputs_data = node_data.get('outputs') outputs_data = node_data.get('outputs')
if outputs_data: if outputs_data:
outputs = [o for o in target_node.outputs if o.type not in IGNORED_SOCKETS] outputs = [o for o in target_node.outputs if o.type not in IGNORED_SOCKETS]
for idx, output in enumerate(outputs): for idx, output in enumerate(outputs_data):
if idx < len(outputs_data) and hasattr(output, "default_value"): if idx < len(outputs) and hasattr(outputs[idx], "default_value"):
loaded_output = outputs_data[idx]
try: try:
if output.type in ['OBJECT', 'COLLECTION']: outputs[idx].default_value = output
output.default_value = get_datablock_from_uuid(loaded_output, None)
else:
output.default_value = loaded_output
except Exception as e: except Exception as e:
logging.warning( logging.warning(
f"Node {target_node.name} output {output.name} parameter not supported, skipping ({e})") f"Node {target_node.name} output {outputs[idx].name} parameter not supported, skipping ({e})")
else: else:
logging.warning( logging.warning(
f"Node {target_node.name} output length mismatch.") f"Node {target_node.name} output length mismatch.")
def dump_node(node: bpy.types.ShaderNode) -> dict:
""" Dump a single node to a dict
:arg node: target node
:type node: bpy.types.Node
:retrun: dict
"""
node_dumper = Dumper()
node_dumper.depth = 1
node_dumper.exclude_filter = [
"dimensions",
"show_expanded",
"name_full",
"select",
"bl_label",
"bl_height_min",
"bl_height_max",
"bl_height_default",
"bl_width_min",
"bl_width_max",
"type",
"bl_icon",
"bl_width_default",
"bl_static_type",
"show_tetxure",
"is_active_output",
"hide",
"show_options",
"show_preview",
"show_texture",
"outputs",
"width_hidden",
"image"
]
dumped_node = node_dumper.dump(node)
if node.parent:
dumped_node['parent'] = node.parent.name
dump_io_needed = (node.type not in ['REROUTE', 'OUTPUT_MATERIAL'])
if dump_io_needed:
io_dumper = Dumper()
io_dumper.depth = 2
io_dumper.include_filter = ["default_value"]
if hasattr(node, 'inputs'):
dumped_node['inputs'] = []
inputs = [i for i in node.inputs if i.type not in IGNORED_SOCKETS]
for idx, inpt in enumerate(inputs):
if hasattr(inpt, 'default_value'):
if isinstance(inpt.default_value, bpy.types.ID):
dumped_input = inpt.default_value.uuid
else:
dumped_input = io_dumper.dump(inpt.default_value)
dumped_node['inputs'].append(dumped_input)
if hasattr(node, 'outputs'):
dumped_node['outputs'] = []
for idx, output in enumerate(node.outputs):
if output.type not in IGNORED_SOCKETS:
if hasattr(output, 'default_value'):
dumped_node['outputs'].append(
io_dumper.dump(output.default_value))
if hasattr(node, 'color_ramp'):
ramp_dumper = Dumper()
ramp_dumper.depth = 4
ramp_dumper.include_filter = [
'elements',
'alpha',
'color',
'position',
'interpolation',
'hue_interpolation',
'color_mode'
]
dumped_node['color_ramp'] = ramp_dumper.dump(node.color_ramp)
if hasattr(node, 'mapping'):
curve_dumper = Dumper()
curve_dumper.depth = 5
curve_dumper.include_filter = [
'curves',
'points',
'location'
]
dumped_node['mapping'] = curve_dumper.dump(node.mapping)
if hasattr(node, 'image') and getattr(node, 'image'):
dumped_node['image_uuid'] = node.image.uuid
if hasattr(node, 'node_tree') and getattr(node, 'node_tree'):
dumped_node['node_tree_uuid'] = node.node_tree.uuid
return dumped_node
def load_links(links_data, node_tree): def load_links(links_data, node_tree):
""" Load node_tree links from a list """ Load node_tree links from a list
@@ -225,6 +120,93 @@ def dump_links(links):
return links_data return links_data
def dump_node(node: bpy.types.ShaderNode) -> dict:
""" Dump a single node to a dict
:arg node: target node
:type node: bpy.types.Node
:retrun: dict
"""
node_dumper = Dumper()
node_dumper.depth = 1
node_dumper.exclude_filter = [
"dimensions",
"show_expanded",
"name_full",
"select",
"bl_label",
"bl_height_min",
"bl_height_max",
"bl_height_default",
"bl_width_min",
"bl_width_max",
"type",
"bl_icon",
"bl_width_default",
"bl_static_type",
"show_tetxure",
"is_active_output",
"hide",
"show_options",
"show_preview",
"show_texture",
"outputs",
"width_hidden",
"image"
]
dumped_node = node_dumper.dump(node)
dump_io_needed = (node.type not in ['REROUTE', 'OUTPUT_MATERIAL'])
if dump_io_needed:
io_dumper = Dumper()
io_dumper.depth = 2
io_dumper.include_filter = ["default_value"]
if hasattr(node, 'inputs'):
dumped_node['inputs'] = []
for idx, inpt in enumerate(node.inputs):
if hasattr(inpt, 'default_value'):
dumped_node['inputs'].append(
io_dumper.dump(inpt.default_value))
if hasattr(node, 'outputs'):
dumped_node['outputs'] = []
for idx, output in enumerate(node.outputs):
if hasattr(output, 'default_value'):
dumped_node['outputs'].append(
io_dumper.dump(output.default_value))
if hasattr(node, 'color_ramp'):
ramp_dumper = Dumper()
ramp_dumper.depth = 4
ramp_dumper.include_filter = [
'elements',
'alpha',
'color',
'position',
'interpolation',
'color_mode'
]
dumped_node['color_ramp'] = ramp_dumper.dump(node.color_ramp)
if hasattr(node, 'mapping'):
curve_dumper = Dumper()
curve_dumper.depth = 5
curve_dumper.include_filter = [
'curves',
'points',
'location'
]
dumped_node['mapping'] = curve_dumper.dump(node.mapping)
if hasattr(node, 'image') and getattr(node, 'image'):
dumped_node['image_uuid'] = node.image.uuid
if hasattr(node, 'node_tree') and getattr(node, 'node_tree'):
dumped_node['node_tree_uuid'] = node.node_tree.uuid
return dumped_node
def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict: def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict:
""" Dump a shader node_tree to a dict including links and nodes """ Dump a shader node_tree to a dict including links and nodes
@@ -281,7 +263,7 @@ def load_node_tree_sockets(sockets: bpy.types.Collection,
""" """
# Check for removed sockets # Check for removed sockets
for socket in sockets: for socket in sockets:
if not [s for s in sockets_data if 'uuid' in socket and socket['uuid'] == s[2]]: if not [s for s in sockets_data if socket['uuid'] == s[2]]:
sockets.remove(socket) sockets.remove(socket)
# Check for new sockets # Check for new sockets
@@ -321,14 +303,6 @@ def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeT
for node in node_tree_data["nodes"]: for node in node_tree_data["nodes"]:
load_node(node_tree_data["nodes"][node], target_node_tree) load_node(node_tree_data["nodes"][node], target_node_tree)
for node_id, node_data in node_tree_data["nodes"].items():
target_node = target_node_tree.nodes.get(node_id, None)
if target_node is None:
continue
elif 'parent' in node_data:
target_node.parent = target_node_tree.nodes[node_data['parent']]
else:
target_node.parent = None
# TODO: load only required nodes links # TODO: load only required nodes links
# Load nodes links # Load nodes links
target_node_tree.links.clear() target_node_tree.links.clear()
@@ -343,8 +317,6 @@ def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list:
def has_node_group(node): return ( def has_node_group(node): return (
hasattr(node, 'node_tree') and node.node_tree) hasattr(node, 'node_tree') and node.node_tree)
def has_texture(node): return (
node.type in ['ATTRIBUTE_SAMPLE_TEXTURE','TEXTURE'] and node.texture)
deps = [] deps = []
for node in node_tree.nodes: for node in node_tree.nodes:
@@ -352,8 +324,6 @@ def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list:
deps.append(node.image) deps.append(node.image)
elif has_node_group(node): elif has_node_group(node):
deps.append(node.node_tree) deps.append(node.node_tree)
elif has_texture(node):
deps.append(node.texture)
return deps return deps
@@ -384,7 +354,10 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_
if mat_uuid is not None: if mat_uuid is not None:
mat_ref = get_datablock_from_uuid(mat_uuid, None) mat_ref = get_datablock_from_uuid(mat_uuid, None)
else: else:
mat_ref = bpy.data.materials[mat_name] mat_ref = bpy.data.materials.get(mat_name, None)
if mat_ref is None:
raise Exception(f"Material {mat_name} doesn't exist")
dst_materials.append(mat_ref) dst_materials.append(mat_ref)
+39 -185
View File
@@ -23,7 +23,6 @@ import mathutils
from replication.exception import ContextError from replication.exception import ContextError
from .bl_datablock import BlDatablock, get_datablock_from_uuid from .bl_datablock import BlDatablock, get_datablock_from_uuid
from .bl_material import IGNORED_SOCKETS
from .dump_anything import ( from .dump_anything import (
Dumper, Dumper,
Loader, Loader,
@@ -31,82 +30,16 @@ from .dump_anything import (
np_dump_collection) np_dump_collection)
SKIN_DATA = [ SKIN_DATA = [
'radius', 'radius',
'use_loose', 'use_loose',
'use_root' 'use_root'
] ]
if bpy.app.version[1] >= 93: def get_input_index(e):
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float) return int(re.findall('[0-9]+', e)[0])
else:
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str)
logging.warning("Geometry node Float parameter not supported in \
blender 2.92.")
def get_node_group_inputs(node_group):
inputs = []
for inpt in node_group.inputs:
if inpt.type in IGNORED_SOCKETS:
continue
else:
inputs.append(inpt)
return inputs
# return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS]
def dump_physics(target: bpy.types.Object)->dict:
"""
Dump all physics settings from a given object excluding modifier
related physics settings (such as softbody, cloth, dynapaint and fluid)
"""
dumper = Dumper()
dumper.depth = 1
physics_data = {}
# Collisions (collision)
if target.collision and target.collision.use:
physics_data['collision'] = dumper.dump(target.collision)
# Field (field)
if target.field and target.field.type != "NONE":
physics_data['field'] = dumper.dump(target.field)
# Rigid Body (rigid_body)
if target.rigid_body:
physics_data['rigid_body'] = dumper.dump(target.rigid_body)
# Rigid Body constraint (rigid_body_constraint)
if target.rigid_body_constraint:
physics_data['rigid_body_constraint'] = dumper.dump(target.rigid_body_constraint)
return physics_data
def load_physics(dumped_settings: dict, target: bpy.types.Object):
""" Load all physics settings from a given object excluding modifier
related physics settings (such as softbody, cloth, dynapaint and fluid)
"""
loader = Loader()
if 'collision' in dumped_settings:
loader.load(target.collision, dumped_settings['collision'])
if 'field' in dumped_settings:
loader.load(target.field, dumped_settings['field'])
if 'rigid_body' in dumped_settings:
if not target.rigid_body:
bpy.ops.rigidbody.object_add({"object": target})
loader.load(target.rigid_body, dumped_settings['rigid_body'])
elif target.rigid_body:
bpy.ops.rigidbody.object_remove({"object": target})
if 'rigid_body_constraint' in dumped_settings:
if not target.rigid_body_constraint:
bpy.ops.rigidbody.constraint_add({"object": target})
loader.load(target.rigid_body_constraint, dumped_settings['rigid_body_constraint'])
elif target.rigid_body_constraint:
bpy.ops.rigidbody.constraint_remove({"object": target})
def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list: def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list:
""" Dump geometry node modifier input properties """ Dump geometry node modifier input properties
@@ -114,14 +47,15 @@ def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list:
:arg modifier: geometry node modifier to dump :arg modifier: geometry node modifier to dump
:type modifier: bpy.type.Modifier :type modifier: bpy.type.Modifier
""" """
inputs_name = [p for p in dir(modifier) if "Input_" in p]
inputs_name.sort(key=get_input_index)
dumped_inputs = [] dumped_inputs = []
for inpt in get_node_group_inputs(modifier.node_group): for inputs_index, input_name in enumerate(inputs_name):
input_value = modifier[inpt.identifier] input_value = modifier[input_name]
dumped_input = None dumped_input = None
if isinstance(input_value, bpy.types.ID): if isinstance(input_value, bpy.types.ID):
dumped_input = input_value.uuid dumped_input = input_value.uuid
elif isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS): elif type(input_value) in [int, str, float]:
dumped_input = input_value dumped_input = input_value
elif hasattr(input_value, 'to_list'): elif hasattr(input_value, 'to_list'):
dumped_input = input_value.to_list() dumped_input = input_value.to_list()
@@ -139,16 +73,18 @@ def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: b
:type target_modifier: bpy.type.Modifier :type target_modifier: bpy.type.Modifier
""" """
for input_index, inpt in enumerate(get_node_group_inputs(target_modifier.node_group)): inputs_name = [p for p in dir(target_modifier) if "Input_" in p]
inputs_name.sort(key=get_input_index)
for input_index, input_name in enumerate(inputs_name):
dumped_value = dumped_modifier['inputs'][input_index] dumped_value = dumped_modifier['inputs'][input_index]
input_value = target_modifier[inpt.identifier] input_value = target_modifier[input_name]
if isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS): if type(input_value) in [int, str, float]:
target_modifier[inpt.identifier] = dumped_value input_value = dumped_value
elif hasattr(input_value, 'to_list'): elif hasattr(input_value, 'to_list'):
for index in range(len(input_value)): for index in range(len(input_value)):
input_value[index] = dumped_value[index] input_value[index] = dumped_value[index]
elif inpt.type in ['COLLECTION', 'OBJECT']: else:
target_modifier[inpt.identifier] = get_datablock_from_uuid( target_modifier[input_name] = get_datablock_from_uuid(
dumped_value, None) dumped_value, None)
@@ -225,24 +161,19 @@ def find_textures_dependencies(modifiers: bpy.types.bpy_prop_collection) -> [bpy
return textures return textures
def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types.NodeTree]: def find_geometry_nodes(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types.NodeTree]:
""" Find geometry nodes dependencies from a modifier stack """ Find geometry nodes group from a modifier stack
:arg modifiers: modifiers collection :arg modifiers: modifiers collection
:type modifiers: bpy.types.bpy_prop_collection :type modifiers: bpy.types.bpy_prop_collection
:return: list of bpy.types.NodeTree pointers :return: list of bpy.types.NodeTree pointers
""" """
dependencies = [] nodes_groups = []
for mod in modifiers: for item in modifiers:
if mod.type == 'NODES' and mod.node_group: if item.type == 'NODES' and item.node_group:
dependencies.append(mod.node_group) nodes_groups.append(item.node_group)
# for inpt in get_node_group_inputs(mod.node_group):
# parameter = mod.get(inpt.identifier)
# if parameter and isinstance(parameter, bpy.types.ID):
# dependencies.append(parameter)
return dependencies
return nodes_groups
def dump_vertex_groups(src_object: bpy.types.Object) -> dict: def dump_vertex_groups(src_object: bpy.types.Object) -> dict:
""" Dump object's vertex groups """ Dump object's vertex groups
@@ -288,7 +219,6 @@ def load_vertex_groups(dumped_vertex_groups: dict, target_object: bpy.types.Obje
for index, weight in vg['vertices']: for index, weight in vg['vertices']:
vertex_group.add([index], weight, 'REPLACE') vertex_group.add([index], weight, 'REPLACE')
class BlObject(BlDatablock): class BlObject(BlDatablock):
bl_id = "objects" bl_id = "objects"
bl_class = bpy.types.Object bl_class = bpy.types.Object
@@ -366,20 +296,9 @@ class BlObject(BlDatablock):
# Load transformation data # Load transformation data
loader.load(target, data) loader.load(target, data)
# Object display fields
if 'display' in data: if 'display' in data:
loader.load(target.display, data['display']) loader.load(target.display, data['display'])
# Parenting
parent_id = data.get('parent_uid')
if parent_id:
parent = get_datablock_from_uuid(parent_id[0], bpy.data.objects[parent_id[1]])
# Avoid reloading
if target.parent != parent and parent is not None:
target.parent = parent
elif target.parent:
target.parent = None
# Pose # Pose
if 'pose' in data: if 'pose' in data:
if not target.pose: if not target.pose:
@@ -424,48 +343,14 @@ class BlObject(BlDatablock):
SKIN_DATA) SKIN_DATA)
if hasattr(target, 'cycles_visibility') \ if hasattr(target, 'cycles_visibility') \
and 'cycles_visibility' in data: and 'cycles_visibility' in data:
loader.load(target.cycles_visibility, data['cycles_visibility']) loader.load(target.cycles_visibility, data['cycles_visibility'])
# TODO: handle geometry nodes input from dump_anything # TODO: handle geometry nodes input from dump_anything
if hasattr(target, 'modifiers'): if hasattr(target, 'modifiers'):
nodes_modifiers = [ nodes_modifiers = [mod for mod in target.modifiers if mod.type == 'NODES']
mod for mod in target.modifiers if mod.type == 'NODES']
for modifier in nodes_modifiers: for modifier in nodes_modifiers:
load_modifier_geometry_node_inputs( load_modifier_geometry_node_inputs(data['modifiers'][modifier.name], modifier)
data['modifiers'][modifier.name], modifier)
particles_modifiers = [
mod for mod in target.modifiers if mod.type == 'PARTICLE_SYSTEM']
for mod in particles_modifiers:
default = mod.particle_system.settings
dumped_particles = data['modifiers'][mod.name]['particle_system']
loader.load(mod.particle_system, dumped_particles)
settings = get_datablock_from_uuid(dumped_particles['settings_uuid'], None)
if settings:
mod.particle_system.settings = settings
# Hack to remove the default generated particle settings
if not default.uuid:
bpy.data.particles.remove(default)
phys_modifiers = [
mod for mod in target.modifiers if mod.type in ['SOFT_BODY', 'CLOTH']]
for mod in phys_modifiers:
loader.load(mod.settings, data['modifiers'][mod.name]['settings'])
# PHYSICS
load_physics(data, target)
transform = data.get('transforms', None)
if transform:
target.matrix_parent_inverse = mathutils.Matrix(
transform['matrix_parent_inverse'])
target.matrix_basis = mathutils.Matrix(transform['matrix_basis'])
target.matrix_local = mathutils.Matrix(transform['matrix_local'])
def _dump_implementation(self, data, instance=None): def _dump_implementation(self, data, instance=None):
assert(instance) assert(instance)
@@ -481,6 +366,7 @@ class BlObject(BlDatablock):
dumper.include_filter = [ dumper.include_filter = [
"name", "name",
"rotation_mode", "rotation_mode",
"parent",
"data", "data",
"library", "library",
"empty_display_type", "empty_display_type",
@@ -495,6 +381,8 @@ class BlObject(BlDatablock):
"color", "color",
"instance_collection", "instance_collection",
"instance_type", "instance_type",
"location",
"scale",
'lock_location', 'lock_location',
'lock_rotation', 'lock_rotation',
'lock_scale', 'lock_scale',
@@ -508,16 +396,12 @@ class BlObject(BlDatablock):
'show_all_edges', 'show_all_edges',
'show_texture_space', 'show_texture_space',
'show_in_front', 'show_in_front',
'type' 'type',
'rotation_quaternion' if instance.rotation_mode == 'QUATERNION' else 'rotation_euler',
] ]
data = dumper.dump(instance) data = dumper.dump(instance)
dumper.include_filter = [
'matrix_parent_inverse',
'matrix_local',
'matrix_basis']
data['transforms'] = dumper.dump(instance)
dumper.include_filter = [ dumper.include_filter = [
'show_shadows', 'show_shadows',
] ]
@@ -527,10 +411,6 @@ class BlObject(BlDatablock):
if self.is_library: if self.is_library:
return data return data
# PARENTING
if instance.parent:
data['parent_uid'] = (instance.parent.uuid, instance.parent.name)
# MODIFIERS # MODIFIERS
if hasattr(instance, 'modifiers'): if hasattr(instance, 'modifiers'):
data["modifiers"] = {} data["modifiers"] = {}
@@ -538,29 +418,12 @@ class BlObject(BlDatablock):
if modifiers: if modifiers:
dumper.include_filter = None dumper.include_filter = None
dumper.depth = 1 dumper.depth = 1
dumper.exclude_filter = ['is_active']
for index, modifier in enumerate(modifiers): for index, modifier in enumerate(modifiers):
dumped_modifier = dumper.dump(modifier) data["modifiers"][modifier.name] = dumper.dump(modifier)
# hack to dump geometry nodes inputs # hack to dump geometry nodes inputs
if modifier.type == 'NODES': if modifier.type == 'NODES':
dumped_inputs = dump_modifier_geometry_node_inputs( dumped_inputs = dump_modifier_geometry_node_inputs(modifier)
modifier) data["modifiers"][modifier.name]['inputs'] = dumped_inputs
dumped_modifier['inputs'] = dumped_inputs
elif modifier.type == 'PARTICLE_SYSTEM':
dumper.exclude_filter = [
"is_edited",
"is_editable",
"is_global_hair"
]
dumped_modifier['particle_system'] = dumper.dump(modifier.particle_system)
dumped_modifier['particle_system']['settings_uuid'] = modifier.particle_system.settings.uuid
elif modifier.type in ['SOFT_BODY', 'CLOTH']:
dumped_modifier['settings'] = dumper.dump(modifier.settings)
data["modifiers"][modifier.name] = dumped_modifier
gp_modifiers = getattr(instance, 'grease_pencil_modifiers', None) gp_modifiers = getattr(instance, 'grease_pencil_modifiers', None)
if gp_modifiers: if gp_modifiers:
@@ -582,7 +445,6 @@ class BlObject(BlDatablock):
'location'] 'location']
gp_mod_data['curve'] = curve_dumper.dump(modifier.curve) gp_mod_data['curve'] = curve_dumper.dump(modifier.curve)
# CONSTRAINTS # CONSTRAINTS
if hasattr(instance, 'constraints'): if hasattr(instance, 'constraints'):
dumper.include_filter = None dumper.include_filter = None
@@ -627,6 +489,7 @@ class BlObject(BlDatablock):
bone_groups[group.name] = dumper.dump(group) bone_groups[group.name] = dumper.dump(group)
data['pose']['bone_groups'] = bone_groups data['pose']['bone_groups'] = bone_groups
# VERTEx GROUP # VERTEx GROUP
if len(instance.vertex_groups) > 0: if len(instance.vertex_groups) > 0:
data['vertex_groups'] = dump_vertex_groups(instance) data['vertex_groups'] = dump_vertex_groups(instance)
@@ -663,8 +526,7 @@ class BlObject(BlDatablock):
if hasattr(object_data, 'skin_vertices') and object_data.skin_vertices: if hasattr(object_data, 'skin_vertices') and object_data.skin_vertices:
skin_vertices = list() skin_vertices = list()
for skin_data in object_data.skin_vertices: for skin_data in object_data.skin_vertices:
skin_vertices.append( skin_vertices.append(np_dump_collection(skin_data.data, SKIN_DATA))
np_dump_collection(skin_data.data, SKIN_DATA))
data['skin_vertices'] = skin_vertices data['skin_vertices'] = skin_vertices
# CYCLE SETTINGS # CYCLE SETTINGS
@@ -679,9 +541,6 @@ class BlObject(BlDatablock):
] ]
data['cycles_visibility'] = dumper.dump(instance.cycles_visibility) data['cycles_visibility'] = dumper.dump(instance.cycles_visibility)
# PHYSICS
data.update(dump_physics(instance))
return data return data
def _resolve_deps_implementation(self): def _resolve_deps_implementation(self):
@@ -690,23 +549,18 @@ class BlObject(BlDatablock):
# Avoid Empty case # Avoid Empty case
if self.instance.data: if self.instance.data:
deps.append(self.instance.data) deps.append(self.instance.data)
if self.instance.parent :
# Particle systems deps.append(self.instance.parent)
for particle_slot in self.instance.particle_systems:
deps.append(particle_slot.settings)
if self.is_library: if self.is_library:
deps.append(self.instance.library) deps.append(self.instance.library)
if self.instance.parent:
deps.append(self.instance.parent)
if self.instance.instance_type == 'COLLECTION': if self.instance.instance_type == 'COLLECTION':
# TODO: uuid based # TODO: uuid based
deps.append(self.instance.instance_collection) deps.append(self.instance.instance_collection)
if self.instance.modifiers: if self.instance.modifiers:
deps.extend(find_textures_dependencies(self.instance.modifiers)) deps.extend(find_textures_dependencies(self.instance.modifiers))
deps.extend(find_geometry_nodes_dependencies(self.instance.modifiers)) deps.extend(find_geometry_nodes(self.instance.modifiers))
return deps return deps
-90
View File
@@ -1,90 +0,0 @@
import bpy
import mathutils
from . import dump_anything
from .bl_datablock import BlDatablock, get_datablock_from_uuid
def dump_textures_slots(texture_slots: bpy.types.bpy_prop_collection) -> list:
""" Dump every texture slot collection as the form:
[(index, slot_texture_uuid, slot_texture_name), (), ...]
"""
dumped_slots = []
for index, slot in enumerate(texture_slots):
if slot and slot.texture:
dumped_slots.append((index, slot.texture.uuid, slot.texture.name))
return dumped_slots
def load_texture_slots(dumped_slots: list, target_slots: bpy.types.bpy_prop_collection):
"""
"""
for index, slot in enumerate(target_slots):
if slot:
target_slots.clear(index)
for index, slot_uuid, slot_name in dumped_slots:
target_slots.create(index).texture = get_datablock_from_uuid(
slot_uuid, slot_name
)
IGNORED_ATTR = [
"is_embedded_data",
"is_evaluated",
"is_fluid",
"is_library_indirect",
"users"
]
class BlParticle(BlDatablock):
bl_id = "particles"
bl_class = bpy.types.ParticleSettings
bl_icon = "PARTICLES"
bl_check_common = False
bl_reload_parent = False
def _construct(self, data):
instance = bpy.data.particles.new(data["name"])
instance.uuid = self.uuid
return instance
def _load_implementation(self, data, target):
dump_anything.load(target, data)
dump_anything.load(target.effector_weights, data["effector_weights"])
# Force field
force_field_1 = data.get("force_field_1", None)
if force_field_1:
dump_anything.load(target.force_field_1, force_field_1)
force_field_2 = data.get("force_field_2", None)
if force_field_2:
dump_anything.load(target.force_field_2, force_field_2)
# Texture slots
load_texture_slots(data["texture_slots"], target.texture_slots)
def _dump_implementation(self, data, instance=None):
assert instance
dumper = dump_anything.Dumper()
dumper.depth = 1
dumper.exclude_filter = IGNORED_ATTR
data = dumper.dump(instance)
# Particle effectors
data["effector_weights"] = dumper.dump(instance.effector_weights)
if instance.force_field_1:
data["force_field_1"] = dumper.dump(instance.force_field_1)
if instance.force_field_2:
data["force_field_2"] = dumper.dump(instance.force_field_2)
# Texture slots
data["texture_slots"] = dump_textures_slots(instance.texture_slots)
return data
def _resolve_deps_implementation(self):
return [t.texture for t in self.instance.texture_slots if t and t.texture]
-2
View File
@@ -610,8 +610,6 @@ class Loader:
instance.write(bpy.data.fonts.get(dump)) instance.write(bpy.data.fonts.get(dump))
elif isinstance(rna_property_type, T.Sound): elif isinstance(rna_property_type, T.Sound):
instance.write(bpy.data.sounds.get(dump)) instance.write(bpy.data.sounds.get(dump))
# elif isinstance(rna_property_type, T.ParticleSettings):
# instance.write(bpy.data.particles.get(dump))
def _load_matrix(self, matrix, dump): def _load_matrix(self, matrix, dump):
matrix.write(mathutils.Matrix(dump)) matrix.write(mathutils.Matrix(dump))
+4 -361
View File
@@ -32,7 +32,6 @@ 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
@@ -40,7 +39,6 @@ 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
@@ -58,226 +56,6 @@ background_execution_queue = Queue()
deleyables = [] deleyables = []
stop_modal_executor = False 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 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
@@ -432,6 +210,8 @@ class SessionStartOperator(bpy.types.Operator):
type_module_class, type_module_class,
check_common=type_module_class.bl_check_common) check_common=type_module_class.bl_check_common)
deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate))
if bpy.app.version[1] >= 91: if bpy.app.version[1] >= 91:
python_binary_path = sys.executable python_binary_path = sys.executable
else: else:
@@ -492,7 +272,6 @@ class SessionStartOperator(bpy.types.Operator):
# Background client updates service # Background client updates service
deleyables.append(timers.ClientUpdate()) deleyables.append(timers.ClientUpdate())
deleyables.append(timers.DynamicRightSelectTimer()) deleyables.append(timers.DynamicRightSelectTimer())
deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate))
# deleyables.append(timers.PushTimer( # deleyables.append(timers.PushTimer(
# queue=stagging, # queue=stagging,
# timeout=settings.depsgraph_update_rate # timeout=settings.depsgraph_update_rate
@@ -1033,26 +812,6 @@ class SessionStopAutoSaveOperator(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
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'}
class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper): class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
bl_idname = "session.load" bl_idname = "session.load"
@@ -1069,42 +828,9 @@ 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:
@@ -1153,16 +879,7 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
logging.info("Graph succefully loaded") logging.info("Graph succefully loaded")
# Persitstent collection utils.clean_scene()
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():
@@ -1172,69 +889,6 @@ 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
bpy.context.scene.eevee.use_bloom = False
return {'FINISHED'} return {'FINISHED'}
@@ -1264,7 +918,6 @@ classes = (
SessionLoadSaveOperator, SessionLoadSaveOperator,
SessionStopAutoSaveOperator, SessionStopAutoSaveOperator,
SessionPurgeOperator, SessionPurgeOperator,
SessionRenderReplay
) )
def update_external_dependencies(): def update_external_dependencies():
@@ -1315,16 +968,6 @@ 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
@@ -1352,7 +995,7 @@ def depsgraph_evaluation(scene):
# - if its ours or ( under common and diff), launch the # - if its ours or ( under common and diff), launch the
# update process # update process
# - if its to someone else, ignore the update # - if its to someone else, ignore the update
if node and (node.owner == session.id or node.bl_check_common): if node and node.owner in [session.id, RP_COMMON]:
if node.state == UP: if node.state == UP:
try: try:
if node.has_changed(): if node.has_changed():
+2 -126
View File
@@ -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,88 +93,6 @@ 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()
@@ -277,7 +195,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
connection_timeout: bpy.props.IntProperty( connection_timeout: bpy.props.IntProperty(
name='connection timeout', name='connection timeout',
description='connection timeout before disconnection', description='connection timeout before disconnection',
default=5000 default=1000
) )
# Replication update settings # Replication update settings
depsgraph_update_rate: bpy.props.FloatProperty( depsgraph_update_rate: bpy.props.FloatProperty(
@@ -612,37 +530,6 @@ 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 = (
@@ -665,20 +552,9 @@ 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
+2 -3
View File
@@ -17,7 +17,7 @@
import logging import logging
import sys import sys
import traceback
import bpy import bpy
from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE, from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE,
STATE_INITIAL, STATE_LOBBY, STATE_QUITTING, STATE_INITIAL, STATE_LOBBY, STATE_QUITTING,
@@ -112,8 +112,7 @@ class ApplyTimer(Timer):
try: try:
session.apply(node) session.apply(node)
except Exception as e: except Exception as e:
logging.error(f"Fail to apply {node_ref.uuid}") logging.error(f"Fail to apply {node_ref.uuid}: {e}")
traceback.print_exc()
else: else:
if node_ref.bl_reload_parent: if node_ref.bl_reload_parent:
for parent in session._graph.find_parents(node): for parent in session._graph.find_parents(node):
-34
View File
@@ -615,39 +615,6 @@ 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,
@@ -657,7 +624,6 @@ 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,
) )
+12
View File
@@ -100,6 +100,18 @@ 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)]
+1 -1
View File
@@ -13,7 +13,7 @@ def main():
if len(sys.argv) > 2: if len(sys.argv) > 2:
blender_rev = sys.argv[2] blender_rev = sys.argv[2]
else: else:
blender_rev = "2.92.0" blender_rev = "2.91.0"
try: try:
exit_val = BAT.test_blender_addon(addon_path=addon, blender_revision=blender_rev) exit_val = BAT.test_blender_addon(addon_path=addon, blender_revision=blender_rev)
+4 -3
View File
@@ -7,7 +7,7 @@ import bpy
import random import random
from multi_user.bl_types.bl_object import BlObject from multi_user.bl_types.bl_object import BlObject
# Removed 'BUILD', 'SOFT_BODY' modifier because the seed doesn't seems to be # Removed 'BUILD' modifier because the seed doesn't seems to be
# correctly initialized (#TODO: report the bug) # correctly initialized (#TODO: report the bug)
MOFIFIERS_TYPES = [ MOFIFIERS_TYPES = [
'DATA_TRANSFER', 'MESH_CACHE', 'MESH_SEQUENCE_CACHE', 'DATA_TRANSFER', 'MESH_CACHE', 'MESH_SEQUENCE_CACHE',
@@ -22,7 +22,8 @@ MOFIFIERS_TYPES = [
'MESH_DEFORM', 'SHRINKWRAP', 'SIMPLE_DEFORM', 'SMOOTH', 'MESH_DEFORM', 'SHRINKWRAP', 'SIMPLE_DEFORM', 'SMOOTH',
'CORRECTIVE_SMOOTH', 'LAPLACIANSMOOTH', 'SURFACE_DEFORM', 'CORRECTIVE_SMOOTH', 'LAPLACIANSMOOTH', 'SURFACE_DEFORM',
'WARP', 'WAVE', 'CLOTH', 'COLLISION', 'DYNAMIC_PAINT', 'WARP', 'WAVE', 'CLOTH', 'COLLISION', 'DYNAMIC_PAINT',
'EXPLODE', 'FLUID', 'OCEAN', 'PARTICLE_INSTANCE', 'SURFACE'] 'EXPLODE', 'FLUID', 'OCEAN', 'PARTICLE_INSTANCE',
'SOFT_BODY', 'SURFACE']
GP_MODIFIERS_TYPE = [ GP_MODIFIERS_TYPE = [
'GP_ARRAY', 'GP_BUILD', 'GP_MIRROR', 'GP_MULTIPLY', 'GP_ARRAY', 'GP_BUILD', 'GP_MIRROR', 'GP_MULTIPLY',
@@ -71,5 +72,5 @@ def test_object(clear_blend):
test = implementation._construct(expected) test = implementation._construct(expected)
implementation._load(expected, test) implementation._load(expected, test)
result = implementation._dump(test) result = implementation._dump(test)
print(DeepDiff(expected, result))
assert not DeepDiff(expected, result) assert not DeepDiff(expected, result)