508 lines
16 KiB
Python
508 lines
16 KiB
Python
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
#
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
|
|
import bpy
|
|
import mathutils
|
|
import logging
|
|
import re
|
|
|
|
from uuid import uuid4
|
|
|
|
from .dump_anything import Loader, Dumper
|
|
from .bl_datablock import get_datablock_from_uuid, stamp_uuid
|
|
from replication.protocol import ReplicatedDatablock
|
|
from replication.objects import Node
|
|
|
|
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
|
|
IGNORED_SOCKETS = ['GEOMETRY', 'SHADER', 'CUSTOM']
|
|
|
|
def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
|
|
""" Load a node into a node_tree from a dict
|
|
|
|
:arg node_data: dumped node data
|
|
:type node_data: dict
|
|
:arg node_tree: datablock node_tree
|
|
:type node_tree: bpy.types.NodeTree
|
|
"""
|
|
loader = Loader()
|
|
target_node = node_tree.nodes.new(type=node_data["bl_idname"])
|
|
target_node.select = False
|
|
loader.load(target_node, node_data)
|
|
image_uuid = node_data.get('image_uuid', None)
|
|
node_tree_uuid = node_data.get('node_tree_uuid', None)
|
|
|
|
if image_uuid and not target_node.image:
|
|
target_node.image = get_datablock_from_uuid(image_uuid, None)
|
|
|
|
if node_tree_uuid:
|
|
target_node.node_tree = get_datablock_from_uuid(node_tree_uuid, None)
|
|
|
|
inputs_data = node_data.get('inputs')
|
|
if inputs_data:
|
|
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"):
|
|
loaded_input = inputs_data[idx]
|
|
try:
|
|
if inpt.type in ['OBJECT', 'COLLECTION']:
|
|
inpt.default_value = get_datablock_from_uuid(loaded_input, None)
|
|
else:
|
|
inpt.default_value = loaded_input
|
|
except Exception as e:
|
|
logging.warning(f"Node {target_node.name} input {inpt.name} parameter not supported, skipping ({e})")
|
|
else:
|
|
logging.warning(f"Node {target_node.name} input length mismatch.")
|
|
|
|
outputs_data = node_data.get('outputs')
|
|
if outputs_data:
|
|
outputs = [o for o in target_node.outputs if o.type not in IGNORED_SOCKETS]
|
|
for idx, output in enumerate(outputs):
|
|
if idx < len(outputs_data) and hasattr(output, "default_value"):
|
|
loaded_output = outputs_data[idx]
|
|
try:
|
|
if output.type in ['OBJECT', 'COLLECTION']:
|
|
output.default_value = get_datablock_from_uuid(loaded_output, None)
|
|
else:
|
|
output.default_value = loaded_output
|
|
except Exception as e:
|
|
logging.warning(
|
|
f"Node {target_node.name} output {output.name} parameter not supported, skipping ({e})")
|
|
else:
|
|
logging.warning(
|
|
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: datablock 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):
|
|
""" Load node_tree links from a list
|
|
|
|
:arg links_data: dumped node links
|
|
:type links_data: list
|
|
:arg node_tree: node links collection
|
|
:type node_tree: bpy.types.NodeTree
|
|
"""
|
|
|
|
for link in links_data:
|
|
input_socket = node_tree.nodes[link['to_node']
|
|
].inputs[int(link['to_socket'])]
|
|
output_socket = node_tree.nodes[link['from_node']].outputs[int(
|
|
link['from_socket'])]
|
|
node_tree.links.new(input_socket, output_socket)
|
|
|
|
|
|
def dump_links(links):
|
|
""" Dump node_tree links collection to a list
|
|
|
|
:arg links: node links collection
|
|
:type links: bpy.types.NodeLinks
|
|
:retrun: list
|
|
"""
|
|
|
|
links_data = []
|
|
|
|
for link in links:
|
|
to_socket = NODE_SOCKET_INDEX.search(
|
|
link.to_socket.path_from_id()).group(1)
|
|
from_socket = NODE_SOCKET_INDEX.search(
|
|
link.from_socket.path_from_id()).group(1)
|
|
links_data.append({
|
|
'to_node': link.to_node.name,
|
|
'to_socket': to_socket,
|
|
'from_node': link.from_node.name,
|
|
'from_socket': from_socket,
|
|
})
|
|
|
|
return links_data
|
|
|
|
|
|
def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict:
|
|
""" Dump a shader node_tree to a dict including links and nodes
|
|
|
|
:arg node_tree: dumped shader node tree
|
|
:type node_tree: bpy.types.ShaderNodeTree
|
|
:return: dict
|
|
"""
|
|
node_tree_data = {
|
|
'nodes': {node.name: dump_node(node) for node in node_tree.nodes},
|
|
'links': dump_links(node_tree.links),
|
|
'name': node_tree.name,
|
|
'type': type(node_tree).__name__
|
|
}
|
|
|
|
for socket_id in ['inputs', 'outputs']:
|
|
socket_collection = getattr(node_tree, socket_id)
|
|
node_tree_data[socket_id] = dump_node_tree_sockets(socket_collection)
|
|
|
|
return node_tree_data
|
|
|
|
|
|
def dump_node_tree_sockets(sockets: bpy.types.Collection) -> dict:
|
|
""" dump sockets of a shader_node_tree
|
|
|
|
:arg target_node_tree: datablock node_tree
|
|
:type target_node_tree: bpy.types.NodeTree
|
|
:arg socket_id: socket identifer
|
|
:type socket_id: str
|
|
:return: dict
|
|
"""
|
|
sockets_data = []
|
|
for socket in sockets:
|
|
try:
|
|
socket_uuid = socket['uuid']
|
|
except Exception:
|
|
socket_uuid = str(uuid4())
|
|
socket['uuid'] = socket_uuid
|
|
|
|
sockets_data.append((socket.name, socket.bl_socket_idname, socket_uuid))
|
|
|
|
return sockets_data
|
|
|
|
|
|
def load_node_tree_sockets(sockets: bpy.types.Collection,
|
|
sockets_data: dict):
|
|
""" load sockets of a shader_node_tree
|
|
|
|
:arg target_node_tree: datablock node_tree
|
|
:type target_node_tree: bpy.types.NodeTree
|
|
:arg socket_id: socket identifer
|
|
:type socket_id: str
|
|
:arg socket_data: dumped socket data
|
|
:type socket_data: dict
|
|
"""
|
|
# Check for removed sockets
|
|
for socket in sockets:
|
|
if not [s for s in sockets_data if 'uuid' in socket and socket['uuid'] == s[2]]:
|
|
sockets.remove(socket)
|
|
|
|
# Check for new sockets
|
|
for idx, socket_data in enumerate(sockets_data):
|
|
try:
|
|
checked_socket = sockets[idx]
|
|
if checked_socket.name != socket_data[0]:
|
|
checked_socket.name = socket_data[0]
|
|
except Exception:
|
|
s = sockets.new(socket_data[1], socket_data[0])
|
|
s['uuid'] = socket_data[2]
|
|
|
|
|
|
def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeTree) -> dict:
|
|
"""Load a shader node_tree from dumped data
|
|
|
|
:arg node_tree_data: dumped node data
|
|
:type node_tree_data: dict
|
|
:arg target_node_tree: datablock node_tree
|
|
:type target_node_tree: bpy.types.NodeTree
|
|
"""
|
|
# TODO: load only required nodes
|
|
target_node_tree.nodes.clear()
|
|
|
|
if not target_node_tree.is_property_readonly('name'):
|
|
target_node_tree.name = node_tree_data['name']
|
|
|
|
if 'inputs' in node_tree_data:
|
|
socket_collection = getattr(target_node_tree, 'inputs')
|
|
load_node_tree_sockets(socket_collection, node_tree_data['inputs'])
|
|
|
|
if 'outputs' in node_tree_data:
|
|
socket_collection = getattr(target_node_tree, 'outputs')
|
|
load_node_tree_sockets(socket_collection, node_tree_data['outputs'])
|
|
|
|
# Load nodes
|
|
for node in node_tree_data["nodes"]:
|
|
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
|
|
# Load nodes links
|
|
target_node_tree.links.clear()
|
|
|
|
load_links(node_tree_data["links"], target_node_tree)
|
|
|
|
|
|
def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list:
|
|
def has_image(node): return (
|
|
node.type in ['TEX_IMAGE', 'TEX_ENVIRONMENT'] and node.image)
|
|
|
|
def has_node_group(node): return (
|
|
hasattr(node, 'node_tree') and node.node_tree)
|
|
|
|
def has_texture(node): return (
|
|
node.type in ['ATTRIBUTE_SAMPLE_TEXTURE','TEXTURE'] and node.texture)
|
|
deps = []
|
|
|
|
for node in node_tree.nodes:
|
|
if has_image(node):
|
|
deps.append(node.image)
|
|
elif has_node_group(node):
|
|
deps.append(node.node_tree)
|
|
elif has_texture(node):
|
|
deps.append(node.texture)
|
|
|
|
return deps
|
|
|
|
|
|
def dump_materials_slots(materials: bpy.types.bpy_prop_collection) -> list:
|
|
""" Dump material slots collection
|
|
|
|
:arg materials: material slots collection to dump
|
|
:type materials: bpy.types.bpy_prop_collection
|
|
:return: list of tuples (mat_uuid, mat_name)
|
|
"""
|
|
return [(m.uuid, m.name) for m in materials if m]
|
|
|
|
|
|
def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_collection):
|
|
""" Load material slots
|
|
|
|
:arg src_materials: dumped material collection (ex: object.materials)
|
|
:type src_materials: list of tuples (uuid, name)
|
|
:arg dst_materials: datablock material collection pointer
|
|
:type dst_materials: bpy.types.bpy_prop_collection
|
|
"""
|
|
# MATERIAL SLOTS
|
|
dst_materials.clear()
|
|
|
|
for mat_uuid, mat_name in src_materials:
|
|
mat_ref = None
|
|
if mat_uuid is not None:
|
|
mat_ref = get_datablock_from_uuid(mat_uuid, None)
|
|
else:
|
|
mat_ref = bpy.data.materials[mat_name]
|
|
|
|
dst_materials.append(mat_ref)
|
|
|
|
|
|
class BlMaterial(ReplicatedDatablock):
|
|
bl_id = "materials"
|
|
bl_class = bpy.types.Material
|
|
bl_check_common = False
|
|
bl_icon = 'MATERIAL_DATA'
|
|
bl_reload_parent = False
|
|
|
|
@staticmethod
|
|
def construct(data: dict) -> object:
|
|
return bpy.data.materials.new(data["name"])
|
|
|
|
@staticmethod
|
|
def load(data: dict, datablock: object):
|
|
data = data
|
|
loader = Loader()
|
|
|
|
is_grease_pencil = data.get('is_grease_pencil')
|
|
use_nodes = data.get('use_nodes')
|
|
|
|
loader.load(datablock, data)
|
|
|
|
if is_grease_pencil:
|
|
if not datablock.is_grease_pencil:
|
|
bpy.data.materials.create_gpencil_data(datablock)
|
|
loader.load(datablock.grease_pencil, data['grease_pencil'])
|
|
elif use_nodes:
|
|
if datablock.node_tree is None:
|
|
datablock.use_nodes = True
|
|
|
|
load_node_tree(data['node_tree'], datablock.node_tree)
|
|
|
|
@staticmethod
|
|
def dump(datablock: object) -> dict:
|
|
stamp_uuid(datablock)
|
|
|
|
mat_dumper = Dumper()
|
|
mat_dumper.depth = 2
|
|
mat_dumper.include_filter = [
|
|
'name',
|
|
'blend_method',
|
|
'shadow_method',
|
|
'alpha_threshold',
|
|
'show_transparent_back',
|
|
'use_backface_culling',
|
|
'use_screen_refraction',
|
|
'use_sss_translucency',
|
|
'refraction_depth',
|
|
'preview_render_type',
|
|
'use_preview_world',
|
|
'pass_index',
|
|
'use_nodes',
|
|
'diffuse_color',
|
|
'specular_color',
|
|
'roughness',
|
|
'specular_intensity',
|
|
'metallic',
|
|
'line_color',
|
|
'line_priority',
|
|
'is_grease_pencil'
|
|
]
|
|
data = mat_dumper.dump(datablock)
|
|
|
|
if datablock.is_grease_pencil:
|
|
gp_mat_dumper = Dumper()
|
|
gp_mat_dumper.depth = 3
|
|
|
|
gp_mat_dumper.include_filter = [
|
|
'color',
|
|
'fill_color',
|
|
'mix_color',
|
|
'mix_factor',
|
|
'mix_stroke_factor',
|
|
# 'texture_angle',
|
|
# 'texture_scale',
|
|
# 'texture_offset',
|
|
'pixel_size',
|
|
'hide',
|
|
'lock',
|
|
'ghost',
|
|
# 'texture_clamp',
|
|
'flip',
|
|
'use_overlap_strokes',
|
|
'show_stroke',
|
|
'show_fill',
|
|
'alignment_mode',
|
|
'pass_index',
|
|
'mode',
|
|
'stroke_style',
|
|
# 'stroke_image',
|
|
'fill_style',
|
|
'gradient_type',
|
|
# 'fill_image',
|
|
'use_stroke_holdout',
|
|
'use_overlap_strokes',
|
|
'use_fill_holdout',
|
|
]
|
|
data['grease_pencil'] = gp_mat_dumper.dump(datablock.grease_pencil)
|
|
elif datablock.use_nodes:
|
|
data['node_tree'] = dump_node_tree(datablock.node_tree)
|
|
|
|
return data
|
|
|
|
@staticmethod
|
|
def resolve_deps(datablock: object) -> [object]:
|
|
# TODO: resolve node group deps
|
|
deps = []
|
|
|
|
if datablock.use_nodes:
|
|
deps.extend(get_node_tree_dependencies(datablock.node_tree))
|
|
|
|
return deps
|
|
|
|
_type = bpy.types.Material
|
|
_class = BlMaterial
|