feat: dramatic reduction in rendering time 🚀
feat: add more support to eevee render fix: fail to add utils node inflate fix: file import button missing chore: change operator names BREAKING CHANGE: remove support to blender 4.0 BREAKING CHANGE: remove support to gpu render with cuda
This commit is contained in:
29
README.md
29
README.md
@@ -7,11 +7,11 @@
|
||||

|
||||

|
||||
|
||||
Bioxel Nodes is a Blender extension for scientific volumetric data visualization. It using Blender's powerful Geometry Nodes and Cycles to process and render volumetric data. You are free to share your blender file to anyone who does not install this extension, since most processes were done by Blender's native nodes.
|
||||
Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It using Blender's powerful Geometry Nodes and Cycles to process and render volumetric data. You are free to share your blender file to anyone who does not install this extension, since most processes were done by Blender's native nodes.
|
||||
|
||||
## About
|
||||
|
||||
Before us, there have been many tutorials and extensions for importing volumetric data into Blender. However, we found that there were many scientific issues that were not addressed in place, and the volume render results were not epic. With Bioxel Nodes, you can easily import any format volumetric data into Blender, and more importantly, make a beautiful realistic volume rendering quickly.
|
||||
Before us, there have been many tutorials and addons for importing volumetric data into Blender. However, we found that there were many scientific issues that were not addressed in place, and the volume render results were not epic. With Bioxel Nodes, you can easily import any format volumetric data into Blender, and more importantly, make a beautiful realistic volume rendering quickly.
|
||||
|
||||
Below are some examples with Bioxel Nodes. Thanks to Cycles Render, the volumetric data can be rendered with great detail:
|
||||
|
||||
@@ -41,30 +41,29 @@ So how to use this extension? please check [Getting Started](https://omoolab.git
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Cycles CPUs only, Cycles GPUs (Optix), EEVEE (partial support)
|
||||
- Sections cannot be generated (will be supported soon)
|
||||
- Time sequence volume not supported (will be supported soon)
|
||||
|
||||
## Upgrade from 0.1.x to 0.2.x
|
||||
## Compatibility to Newer Version
|
||||
|
||||
You need to do the following:
|
||||
Addon are updating, and it is possible that newer versions of the addon will not work on old project files properly. In order to make the old files work, you can do the following:
|
||||
|
||||
1. Remove the old version of Bioxel Nodes at Preferences > Add-ons
|
||||
2. Add the new version and restart Blender.
|
||||
### For project files that need to be archived
|
||||
|
||||
It is not support editing the same blender file across extension versions. In order to make sure that the previous file works properly. You need to save the staged data before upgrading ( read the last section of [Getting Started](https://omoolab.github.io/BioxelNodes/latest/getting-started/#share-your-file) ).
|
||||
Persistently save the Addon nodes before the addon is updated. This will put the nodes out of sync with the addon functionality, but it will ensure that the entire file can be calculated and rendered correctly, it fit to project files that you need to archive.
|
||||
Bioxel Nodes > Save All Staged Data
|
||||
|
||||
But even then, there is still no guarantee that the new version of the extension will work on the old blender file. Therefore, it is highly recommended to open a new blender file to start the creating, not based on the old one.
|
||||
### For working project files
|
||||
|
||||
Alternatively, objects from the old file that have nothing to do with Bioxel Nodes could be append to the new blender file.
|
||||
If you've ever done a persistent save of Bioxel Nodes nodes, it's possible that after the addon update, there may be new features of the addon that don't synergize with the saved nodes. In order for the new version to work with the old nodes, you need to relink them.
|
||||
Bioxel Nodes > Relink Nodes to Addon
|
||||
|
||||
## About EEVEE Render
|
||||
|
||||
Bioxel Nodes is designed for Cycles Render. However, it does support eevee render partially. "Solid Shader" node and "Volume Shader" node have a toggle called "EEVEE Render". If you want to render Bioxel Component in real-time, turn it on.
|
||||
|
||||
Also, there are some limitations:
|
||||
Bioxel Nodes is designed for Cycles Render. However, it does support eevee render partially.Also, there are some limitations:
|
||||
|
||||
1. Only one cutter supported.
|
||||
2. You cannot use "Color Ramp" over 2 colors.
|
||||
3. EEVEE Render result is not that great as Cycles does.
|
||||
2. EEVEE Render result is not that great as Cycles does.
|
||||
|
||||
> "Volume Shader" node is not work properly in EEVEE Next since 4.2. It is because EEVEE Next is not support attributes from instances of volume shader by now. But the Blender 4.2 docs still say attributes reading is ok, so I suppose this feature will eventually be implemented.
|
||||
> Volume Shader is not work properly in EEVEE Next since 4.2. It is because EEVEE Next is not support attributes from instances of volume shader by now. But Blender 4.3 is ok, so I suppose this issue will eventually be fixed.
|
||||
|
||||
@@ -8,8 +8,8 @@ bl_info = {
|
||||
"name": "Bioxel Nodes",
|
||||
"author": "Ma Nan",
|
||||
"description": "",
|
||||
"blender": (4, 0, 0),
|
||||
"version": (0, 2, 4),
|
||||
"blender": (4, 1, 0),
|
||||
"version": (0, 2, 5),
|
||||
"location": "File -> Import",
|
||||
"warning": "",
|
||||
"category": "Node"
|
||||
@@ -21,11 +21,6 @@ auto_load.init()
|
||||
def register():
|
||||
auto_load.register()
|
||||
menus.add()
|
||||
bpy.types.Scene.bioxel_layer_dir = bpy.props.StringProperty(
|
||||
name="Layer Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,62 +4,12 @@ from pathlib import Path
|
||||
from .nodes import AddCustomNode
|
||||
|
||||
|
||||
class SaveAllNodes(bpy.types.Operator):
|
||||
bl_idname = "customnodes.save_all_nodes"
|
||||
bl_label = "Save All Nodes"
|
||||
bl_description = "Save All Custom Nodes to Directory."
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
files = []
|
||||
for classname in dir(bpy.types):
|
||||
if "CUSTOMNODES_MT_NODES_" in classname:
|
||||
cls = getattr(bpy.types, classname)
|
||||
files.append(cls.nodes_file)
|
||||
files = list(set(files))
|
||||
|
||||
for file in files:
|
||||
file_name = Path(file).name
|
||||
# "//"
|
||||
customnodes_node_dir = bpy.path.abspath(context.scene.customnodes_node_dir)
|
||||
|
||||
output_path: Path = Path(customnodes_node_dir, file_name).resolve()
|
||||
source_path: Path = Path(file).resolve()
|
||||
|
||||
if output_path != source_path:
|
||||
shutil.copy(source_path, output_path)
|
||||
|
||||
for lib in bpy.data.libraries:
|
||||
lib_path = str(Path(bpy.path.abspath(lib.filepath)).resolve())
|
||||
if lib_path == file:
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
lib.filepath = bpy.path.relpath(
|
||||
str(output_path), start=str(blend_path))
|
||||
|
||||
self.report({"INFO"}, f"Successfully saved to {output_path}")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class CUSTOMNODES_PT_CustomNodes(bpy.types.Panel):
|
||||
bl_idname = "CUSTOMNODES_PT_CustomNodes"
|
||||
bl_label = "Custom Nodes"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
scene = context.scene
|
||||
layout.prop(scene, 'customnodes_node_dir')
|
||||
layout.operator(SaveAllNodes.bl_idname)
|
||||
|
||||
|
||||
class CustomNodes():
|
||||
def __init__(
|
||||
self,
|
||||
menu_items,
|
||||
nodes_file,
|
||||
class_prefix="CUSTOMNODES_MT_NODES",
|
||||
root_label='CustomNodes',
|
||||
root_icon='NONE'
|
||||
) -> None:
|
||||
@@ -68,9 +18,10 @@ class CustomNodes():
|
||||
|
||||
self.menu_items = menu_items
|
||||
self.nodes_file = str(Path(nodes_file).resolve())
|
||||
|
||||
self.class_prefix = class_prefix
|
||||
self.root_label = root_label
|
||||
self.root_icon = root_icon
|
||||
self.class_prefix = class_prefix
|
||||
|
||||
menu_classes = []
|
||||
self._create_menu_class(
|
||||
@@ -80,7 +31,7 @@ class CustomNodes():
|
||||
)
|
||||
self.menu_classes = menu_classes
|
||||
|
||||
idname = f"CUSTOMNODES_MT_NODES_{root_label.replace(' ', '').upper()}"
|
||||
idname = f"{class_prefix}_{root_label.replace(' ', '').upper()}"
|
||||
|
||||
def add_node_menu(self, context):
|
||||
if ('GeometryNodeTree' == bpy.context.area.spaces[0].tree_type):
|
||||
@@ -92,7 +43,7 @@ class CustomNodes():
|
||||
|
||||
def _create_menu_class(self, menu_classes, items, label='CustomNodes', icon=0, idname_namespace=None):
|
||||
nodes_file = self.nodes_file
|
||||
idname_namespace = idname_namespace or "CUSTOMNODES_MT_NODES"
|
||||
idname_namespace = idname_namespace or self.class_prefix
|
||||
idname = f"{idname_namespace}_{label.replace(' ', '').upper()}"
|
||||
|
||||
# create submenu class if item is menu.
|
||||
@@ -161,7 +112,7 @@ class CustomNodes():
|
||||
self._find_item(found_items, self.menu_items, node_type)
|
||||
return found_items[0] if len(found_items) > 0 else None
|
||||
|
||||
def add_node(self, node_tree, node_type: str):
|
||||
def add_node(self, node_group, node_type: str):
|
||||
item = self.find_item(node_type)
|
||||
op = AddCustomNode()
|
||||
op.nodes_file = self.nodes_file
|
||||
@@ -174,7 +125,7 @@ class CustomNodes():
|
||||
op.node_label = ""
|
||||
op.node_link = True
|
||||
op.node_callback = ""
|
||||
return op.add_node(node_tree)
|
||||
return op.add_node(node_group)
|
||||
|
||||
def register(self):
|
||||
for cls in self.menu_classes:
|
||||
|
||||
@@ -85,9 +85,14 @@ class AddCustomNode():
|
||||
except:
|
||||
...
|
||||
|
||||
def add_node(self, node_tree):
|
||||
def add_node(self, node_group):
|
||||
# Deselect all nodes first
|
||||
for node in node_group.nodes:
|
||||
if node.select:
|
||||
node.select = False
|
||||
|
||||
self.get_node_tree(self.node_type, self.node_link)
|
||||
node = node_tree.new("GeometryNodeGroup")
|
||||
node = node_group.nodes.new("GeometryNodeGroup")
|
||||
self.assign_node_tree(node)
|
||||
|
||||
return node
|
||||
|
||||
@@ -13,7 +13,7 @@ from . import skimage as ski
|
||||
from .nodes import custom_nodes
|
||||
from .props import BIOXELNODES_Series
|
||||
from .utils import (calc_bbox_verts, get_all_layers, get_container_from_selection, get_layer, get_text_index_str,
|
||||
get_node_by_type, hide_in_ray, lock_transform, save_vdb, show_message)
|
||||
get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, show_message)
|
||||
|
||||
try:
|
||||
import SimpleITK as sitk
|
||||
@@ -233,6 +233,8 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
meta['spacing'][2] / orig_spacing[2] * bioxel_size
|
||||
)
|
||||
|
||||
dtype_index = volume.dtype.num
|
||||
|
||||
layer_shape = get_layer_shape(
|
||||
bioxel_size, meta['shape'], orig_spacing)
|
||||
|
||||
@@ -285,10 +287,9 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
)
|
||||
container = bpy.context.active_object
|
||||
|
||||
bbox_verts = calc_bbox_verts((0, 0, 0), volume.shape)
|
||||
bbox_verts = calc_bbox_verts((0, 0, 0), layer_shape)
|
||||
for index, vert in enumerate(container.data.vertices):
|
||||
bbox_transform = transfrom
|
||||
vert.co = bbox_transform @ mathutils.Vector(bbox_verts[index])
|
||||
vert.co = transfrom @ mathutils.Vector(bbox_verts[index])
|
||||
|
||||
container.matrix_world = mat_ras2blender @ mat_scene_scale
|
||||
container.name = container_name
|
||||
@@ -297,14 +298,14 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
container['bioxel_container'] = True
|
||||
container['scene_scale'] = scene_scale
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
container_node_tree = container.modifiers[0].node_group
|
||||
input_node = get_node_by_type(container_node_tree.nodes,
|
||||
container_node_group = container.modifiers[0].node_group
|
||||
input_node = get_nodes_by_type(container_node_group,
|
||||
'NodeGroupInput')[0]
|
||||
container_node_tree.links.remove(input_node.outputs[0].links[0])
|
||||
container_node_group.links.remove(input_node.outputs[0].links[0])
|
||||
|
||||
else:
|
||||
container = bpy.data.objects[self.container]
|
||||
container_node_tree = container.modifiers[0].node_group
|
||||
container_node_group = container.modifiers[0].node_group
|
||||
|
||||
preferences = context.preferences.addons[__package__].preferences
|
||||
loc, rot, sca = transfrom.decompose()
|
||||
@@ -316,7 +317,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
grid = vdb.FloatGrid()
|
||||
volume = volume.copy().astype(np.float32)
|
||||
grid.copyFromArray(volume)
|
||||
grid.transform = vdb.createLinearTransform(transfrom.transposed())
|
||||
# grid.transform = vdb.createLinearTransform(transfrom.transposed())
|
||||
grid.name = layer_type
|
||||
|
||||
vdb_path = save_vdb([grid], context)
|
||||
@@ -353,26 +354,31 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
print(f"Creating layer ...")
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
node_tree = layer.modifiers[0].node_group
|
||||
nodes = node_tree.nodes
|
||||
links = node_tree.links
|
||||
node_group = layer.modifiers[0].node_group
|
||||
|
||||
input_node = get_node_by_type(nodes, 'NodeGroupInput')[0]
|
||||
output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0]
|
||||
input_node = get_nodes_by_type(node_group, 'NodeGroupInput')[0]
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
|
||||
to_layer_node = custom_nodes.add_node(nodes,
|
||||
to_layer_node = custom_nodes.add_node(node_group,
|
||||
"BioxelNodes__ConvertToLayer")
|
||||
|
||||
links.new(input_node.outputs[0], to_layer_node.inputs[0])
|
||||
links.new(to_layer_node.outputs[0], output_node.inputs[0])
|
||||
node_group.links.new(input_node.outputs[0],
|
||||
to_layer_node.inputs[0])
|
||||
node_group.links.new(to_layer_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
|
||||
# for compatibility to old vdb
|
||||
to_layer_node.inputs['Not Transfromed'].default_value = True
|
||||
to_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
|
||||
200000000)
|
||||
to_layer_node.inputs['Data Type'].default_value = dtype_index
|
||||
to_layer_node.inputs['Bioxel Size'].default_value = bioxel_size
|
||||
to_layer_node.inputs['Shape'].default_value = layer_shape
|
||||
to_layer_node.inputs['Origin'].default_value = layer_origin
|
||||
to_layer_node.inputs['Rotation'].default_value = layer_rotation
|
||||
|
||||
move_node_between_nodes(to_layer_node, [input_node, output_node])
|
||||
|
||||
return layer
|
||||
|
||||
if self.read_as == "labels":
|
||||
@@ -386,29 +392,29 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
for i in range(orig_max):
|
||||
label = volume == np.full_like(volume, i+1)
|
||||
print(f"Resampling...")
|
||||
label = ski.resize(label.astype(np.float32),
|
||||
label = ski.resize(label,
|
||||
layer_shape,
|
||||
anti_aliasing=True)
|
||||
preserve_range=True,
|
||||
anti_aliasing=False)
|
||||
layer = create_layer(volume=label,
|
||||
layer_name=f"{container_name}_{layer_name}_{i+1}",
|
||||
layer_type="label")
|
||||
|
||||
# Deselect all nodes first
|
||||
for node in container_node_tree.nodes:
|
||||
if node.select:
|
||||
node.select = False
|
||||
|
||||
mask_node = custom_nodes.add_node(container_node_tree.nodes,
|
||||
mask_node = custom_nodes.add_node(container_node_group,
|
||||
'BioxelNodes_MaskByLabel')
|
||||
mask_node.label = f"{layer_name}_{i+1}"
|
||||
mask_node.inputs[0].default_value = layer
|
||||
|
||||
# Connect to output if no output linked
|
||||
output_node = get_node_by_type(container_node_tree.nodes,
|
||||
output_node = get_nodes_by_type(container_node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
container_node_tree.links.new(mask_node.outputs[0],
|
||||
container_node_group.links.new(mask_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(mask_node, output_node, (-300, 0))
|
||||
else:
|
||||
move_node_to_node(mask_node, output_node,
|
||||
(0, -100 * (i+1)))
|
||||
|
||||
else:
|
||||
if volume.ndim == 4:
|
||||
@@ -442,27 +448,27 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
layer_name=f"{container_name}_{layer_name}",
|
||||
layer_type="scalar")
|
||||
|
||||
layer_node_tree = layer.modifiers[0].node_group
|
||||
to_layer_node = layer_node_tree.nodes['BioxelNodes__ConvertToLayer']
|
||||
layer_node_group = layer.modifiers[0].node_group
|
||||
to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer']
|
||||
to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
|
||||
to_layer_node.inputs['Scalar Max'].default_value = orig_max
|
||||
to_layer_node.inputs['Scalar Min'].default_value = orig_min
|
||||
|
||||
# Deselect all nodes first
|
||||
for node in container_node_tree.nodes:
|
||||
if node.select:
|
||||
node.select = False
|
||||
mask_node = custom_nodes.add_node(container_node_tree.nodes,
|
||||
mask_node = custom_nodes.add_node(container_node_group,
|
||||
'BioxelNodes_MaskByThreshold')
|
||||
mask_node.label = layer_name
|
||||
mask_node.inputs[0].default_value = layer
|
||||
|
||||
# Connect to output if no output linked
|
||||
output_node = get_node_by_type(container_node_tree.nodes,
|
||||
output_node = get_nodes_by_type(container_node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
container_node_tree.links.new(mask_node.outputs[0],
|
||||
container_node_group.links.new(mask_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(mask_node, output_node, (-300, 0))
|
||||
else:
|
||||
move_node_to_node(mask_node, output_node, (0, -100))
|
||||
|
||||
bpy.context.view_layer.objects.active = container
|
||||
|
||||
@@ -470,6 +476,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
if preferences.do_change_render_setting and is_first_import:
|
||||
bpy.context.scene.render.engine = 'CYCLES'
|
||||
try:
|
||||
bpy.context.scene.cycles.shading_system = True
|
||||
bpy.context.scene.cycles.volume_bounces = 12
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 10
|
||||
@@ -489,8 +496,8 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
volume_dtype = "Label"
|
||||
elif self.read_as == "scalar":
|
||||
volume_dtype = "Scalar"
|
||||
title = f"Import '{volume_dtype}' Layer (Add to Container: {self.container})" \
|
||||
if self.container != "" else f"Import '{volume_dtype}' Layer (Init a Container)"
|
||||
title = f"Import as **{volume_dtype}** (Add to Container: {self.container})" \
|
||||
if self.container != "" else f"Import as **{volume_dtype}** (Init a Container)"
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500,
|
||||
title=title)
|
||||
@@ -512,10 +519,9 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m"
|
||||
|
||||
layout = self.layout
|
||||
panel = layout.box()
|
||||
if self.container == "":
|
||||
panel.prop(self, "container_name")
|
||||
panel.prop(self, "layer_name")
|
||||
layout.prop(self, "container_name")
|
||||
layout.prop(self, "layer_name")
|
||||
|
||||
panel = layout.box()
|
||||
panel.prop(self, "bioxel_size")
|
||||
@@ -575,7 +581,16 @@ class ParseVolumeData(bpy.types.Operator):
|
||||
|
||||
print("Collecting Meta Data...")
|
||||
volume, meta = parse_volume_data(self.filepath)
|
||||
do_orient = ext not in SEQUENCE_EXTS or ext in DICOM_EXTS
|
||||
if self.read_as == "labels":
|
||||
not_int = volume.dtype.kind != "b" and volume.dtype.kind != "i" and volume.dtype.kind != "u"
|
||||
too_large = np.max(volume) > 100
|
||||
|
||||
if not_int or too_large:
|
||||
self.report(
|
||||
{"WARNING"}, "This volume data does not looks like labels, please check again.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# do_orient = ext not in SEQUENCE_EXTS or ext in DICOM_EXTS
|
||||
|
||||
orig_shape = meta['shape']
|
||||
orig_spacing = meta['spacing']
|
||||
@@ -601,7 +616,7 @@ class ParseVolumeData(bpy.types.Operator):
|
||||
orig_spacing=orig_spacing,
|
||||
bioxel_size=bioxel_size,
|
||||
series_id=self.series_id or "",
|
||||
do_orient=do_orient,
|
||||
# do_orient=do_orient,
|
||||
container=self.container,
|
||||
read_as=self.read_as,
|
||||
scene_scale=scene_scale
|
||||
@@ -619,7 +634,7 @@ class ParseVolumeData(bpy.types.Operator):
|
||||
if not self.filepath and not self.directory:
|
||||
return {'CANCELLED'}
|
||||
|
||||
show_message('Reading image data, it may take a while...',
|
||||
show_message('Parsing volume data, it may take a while...',
|
||||
'Please be patient...')
|
||||
|
||||
if get_ext(self.filepath) == '.dcm':
|
||||
@@ -675,7 +690,8 @@ class ImportVolumeData():
|
||||
bpy.ops.bioxelnodes.parse_volume_data(
|
||||
'INVOKE_DEFAULT',
|
||||
filepath=self.filepath,
|
||||
directory=self.directory
|
||||
directory=self.directory,
|
||||
read_as=self.read_as
|
||||
)
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -745,7 +761,7 @@ class AddVolumeData(bpy.types.Operator):
|
||||
|
||||
class ExportVolumeData(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.export_volume_data"
|
||||
bl_label = "Export Layer"
|
||||
bl_label = "Export Bioxel as VDB"
|
||||
bl_description = "Export Bioxel Layer as VDB"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import bpy
|
||||
|
||||
from .operators import (AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels,
|
||||
ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange)
|
||||
from .io import ExportVolumeData, ImportAsLabelLayer, ImportAsScalarLayer, ImportVolumeData, AddVolumeData
|
||||
from .misc import SaveLayers
|
||||
from .save import ReLinkNodes, SaveLayers, SaveAllToShare
|
||||
|
||||
|
||||
class ModifyLayer(bpy.types.Menu):
|
||||
@@ -20,7 +21,7 @@ class ModifyLayer(bpy.types.Menu):
|
||||
|
||||
class AddCutterMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_CUTTERS"
|
||||
bl_label = "Add Cutter"
|
||||
bl_label = "Add a Cutter to Container"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -32,7 +33,7 @@ class AddCutterMenu(bpy.types.Menu):
|
||||
|
||||
class ImportLayerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_LAYERS"
|
||||
bl_label = "Import Layer"
|
||||
bl_label = "Import Volume Data as Bioxel"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -68,28 +69,28 @@ class BioxelNodesOutlinerMenu(bpy.types.Menu):
|
||||
layout.operator(SaveLayers.bl_idname)
|
||||
|
||||
|
||||
# def TOPBAR_FILE_IMPORT(self, context):
|
||||
# layout = self.layout
|
||||
# layout.separator()
|
||||
# layout.operator(ImportVolumeData.bl_idname)
|
||||
def TOPBAR_FILE_IMPORT(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu(ImportLayerMenu.bl_idname)
|
||||
|
||||
|
||||
# def TOPBAR_FILE_EXPORT(self, context):
|
||||
# layout = self.layout
|
||||
# layout.separator()
|
||||
# layout.operator(ExportVolumeData.bl_idname)
|
||||
def TOPBAR_FILE_EXPORT(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.operator(ExportVolumeData.bl_idname)
|
||||
|
||||
|
||||
def VIEW3D_OBJECT(self, context):
|
||||
layout = self.layout
|
||||
layout.menu(BioxelNodesView3DMenu.bl_idname)
|
||||
layout.separator()
|
||||
layout.menu(BioxelNodesView3DMenu.bl_idname, icon="FILE_VOLUME")
|
||||
|
||||
|
||||
def OUTLINER_OBJECT(self, context):
|
||||
layout = self.layout
|
||||
layout.menu(BioxelNodesOutlinerMenu.bl_idname)
|
||||
layout.separator()
|
||||
layout.menu(BioxelNodesOutlinerMenu.bl_idname, icon="FILE_VOLUME")
|
||||
|
||||
|
||||
class BioxelNodesTopbarMenu(bpy.types.Menu):
|
||||
@@ -104,7 +105,8 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
|
||||
layout.menu(AddCutterMenu.bl_idname)
|
||||
layout.operator(ConvertToMesh.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(SaveLayers.bl_idname)
|
||||
layout.operator(SaveAllToShare.bl_idname)
|
||||
layout.operator(ReLinkNodes.bl_idname)
|
||||
|
||||
|
||||
def TOPBAR(self, context):
|
||||
@@ -113,16 +115,16 @@ def TOPBAR(self, context):
|
||||
|
||||
|
||||
def add():
|
||||
# bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT)
|
||||
# bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT)
|
||||
bpy.types.OUTLINER_MT_object.prepend(OUTLINER_OBJECT)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.prepend(VIEW3D_OBJECT)
|
||||
bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT)
|
||||
bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT)
|
||||
bpy.types.OUTLINER_MT_object.append(OUTLINER_OBJECT)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(VIEW3D_OBJECT)
|
||||
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR)
|
||||
|
||||
|
||||
def remove():
|
||||
# bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT)
|
||||
# bpy.types.TOPBAR_MT_file_export.remove(TOPBAR_FILE_EXPORT)
|
||||
bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT)
|
||||
bpy.types.TOPBAR_MT_file_export.remove(TOPBAR_FILE_EXPORT)
|
||||
bpy.types.OUTLINER_MT_object.remove(OUTLINER_OBJECT)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(VIEW3D_OBJECT)
|
||||
bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR)
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from .utils import get_all_layers, get_container, get_container_layers
|
||||
|
||||
|
||||
def save_layer(layer, output_dir):
|
||||
name = layer.name
|
||||
|
||||
# "//"
|
||||
output_dir = bpy.path.abspath(output_dir)
|
||||
source_dir = bpy.path.abspath(layer.data.filepath)
|
||||
|
||||
output_path: Path = Path(output_dir, f"{name}.vdb").resolve()
|
||||
source_path: Path = Path(source_dir).resolve()
|
||||
|
||||
if output_path != source_path:
|
||||
shutil.copy(source_path, output_path)
|
||||
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
layer.data.filepath = bpy.path.relpath(
|
||||
str(output_path), start=str(blend_path))
|
||||
|
||||
|
||||
class SaveLayers(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_layers"
|
||||
bl_label = "Save Layers"
|
||||
bl_description = "Save Bioxel Layers to Directory."
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
container = get_container(bpy.context.active_object)
|
||||
return True if container else False
|
||||
|
||||
def execute(self, context):
|
||||
container = get_container(bpy.context.active_object)
|
||||
|
||||
if not container:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
for layer in get_container_layers(container):
|
||||
try:
|
||||
save_layer(layer, context.scene.bioxel_layer_dir)
|
||||
except:
|
||||
self.report(
|
||||
{"WARNING"}, f"Fail to save {layer.name}, skiped")
|
||||
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveAllLayers(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_all_layers"
|
||||
bl_label = "Save All Layers"
|
||||
bl_description = "Save All Bioxel Layers to Directory."
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
layers = get_all_layers()
|
||||
|
||||
if len(layers) == 0:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
for layer in layers:
|
||||
try:
|
||||
save_layer(layer, context.scene.bioxel_layer_dir)
|
||||
except:
|
||||
self.report(
|
||||
{"WARNING"}, f"Fail to save {layer.name}, skiped")
|
||||
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class BIOXELNODES_PT_Bioxels(bpy.types.Panel):
|
||||
bl_idname = "BIOXELNODES_PT_Bioxels"
|
||||
bl_label = "Bioxel Nodes"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "scene"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
scene = context.scene
|
||||
layout.prop(scene, 'bioxel_layer_dir')
|
||||
layout.operator(SaveAllLayers.bl_idname)
|
||||
@@ -56,10 +56,9 @@ import bpy
|
||||
# return callback_str
|
||||
|
||||
|
||||
if bpy.app.version >= (4, 1, 0):
|
||||
NODE_FILE = "BioxelNodes_4.1"
|
||||
NODE_FILE = "BioxelNodes_4.1"
|
||||
|
||||
MENU_ITEMS = [
|
||||
MENU_ITEMS = [
|
||||
{
|
||||
'label': 'Methods',
|
||||
'icon': 'OUTLINER_DATA_VOLUME',
|
||||
@@ -127,7 +126,13 @@ if bpy.app.version >= (4, 1, 0):
|
||||
{
|
||||
'label': 'Color Presets',
|
||||
'icon': 'COLOR',
|
||||
'node_type': 'BioxelNodes_SetColorPresets',
|
||||
'node_type': 'BioxelNodes_ColorPresets',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Color Presets MRI',
|
||||
'icon': 'COLOR',
|
||||
'node_type': 'BioxelNodes_ColorPresets_MRI',
|
||||
'node_description': ''
|
||||
},
|
||||
"separator",
|
||||
@@ -206,28 +211,28 @@ if bpy.app.version >= (4, 1, 0):
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'To Mesh',
|
||||
'label': 'Pick Mesh',
|
||||
'icon': 'OUTLINER_OB_MESH',
|
||||
'node_type': 'BioxelNodes_ToMesh',
|
||||
'node_type': 'BioxelNodes_PickMesh',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'To Volume',
|
||||
'label': 'Pick Volume',
|
||||
'icon': 'OUTLINER_OB_VOLUME',
|
||||
'node_type': 'BioxelNodes_ToVolume',
|
||||
'node_type': 'BioxelNodes_PickVolume',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'To Bbox Wire',
|
||||
'label': 'Pick Bbox Wire',
|
||||
'icon': 'MESH_CUBE',
|
||||
'node_type': 'BioxelNodes_ToBboxWire',
|
||||
'node_type': 'BioxelNodes_PickBboxWire',
|
||||
'node_description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Inflate',
|
||||
'icon': 'OUTLINER_OB_META',
|
||||
'node_type': 'META_DATA',
|
||||
'node_type': 'BioxelNodes_M_Inflate',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
@@ -244,150 +249,13 @@ if bpy.app.version >= (4, 1, 0):
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
else:
|
||||
NODE_FILE = "BioxelNodes_4.0"
|
||||
MENU_ITEMS = [
|
||||
{
|
||||
'label': 'Methods',
|
||||
'icon': 'OUTLINER_DATA_VOLUME',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Mask by Threshold',
|
||||
'icon': 'EMPTY_SINGLE_ARROW',
|
||||
'node_type': 'BioxelNodes_MaskByThreshold',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Mask by Range',
|
||||
'icon': 'IPO_CONSTANT',
|
||||
'node_type': 'BioxelNodes_MaskByRange',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Mask by Label',
|
||||
'icon': 'MESH_CAPSULE',
|
||||
'node_type': 'BioxelNodes_MaskByLabel',
|
||||
'node_description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Shaders',
|
||||
'icon': 'SHADING_RENDERED',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Solid Shader',
|
||||
'icon': 'SHADING_SOLID',
|
||||
'node_type': 'BioxelNodes_AssignSolidShader',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Slime Shader',
|
||||
'icon': 'OUTLINER_OB_META',
|
||||
'node_type': 'BioxelNodes_AssignSlimeShader',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Volume Shader',
|
||||
'icon': 'OUTLINER_OB_VOLUME',
|
||||
'node_type': 'BioxelNodes_AssignVolumeShader',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Universal Shader',
|
||||
'icon': 'SHADING_RENDERED',
|
||||
'node_type': 'BioxelNodes_AssignUniversalShader',
|
||||
'node_description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Colors',
|
||||
'icon': 'COLOR',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Color Ramp 2',
|
||||
'icon': 'IPO_QUAD',
|
||||
'node_type': 'BioxelNodes_SetColorRamp2',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Color Ramp 3',
|
||||
'icon': 'IPO_CUBIC',
|
||||
'node_type': 'BioxelNodes_SetColorRamp3',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Color Ramp 4',
|
||||
'icon': 'IPO_QUART',
|
||||
'node_type': 'BioxelNodes_SetColorRamp4',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Color Ramp 5',
|
||||
'icon': 'IPO_QUINT',
|
||||
'node_type': 'BioxelNodes_SetColorRamp5',
|
||||
'node_description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Cutters',
|
||||
'icon': 'MOD_BEVEL',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Cut',
|
||||
'icon': 'MOD_BEVEL',
|
||||
'node_type': 'BioxelNodes_Cut',
|
||||
'node_description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Plane Cutter',
|
||||
'icon': 'MESH_PLANE',
|
||||
'node_type': 'BioxelNodes_PlaneObjectCutter',
|
||||
'node_description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Cylinder Cutter',
|
||||
'icon': 'MESH_CYLINDER',
|
||||
'node_type': 'BioxelNodes_CylinderObjectCutter',
|
||||
'node_description': '',
|
||||
},
|
||||
{
|
||||
'label': 'Cube Cutter',
|
||||
'icon': 'MESH_CUBE',
|
||||
'node_type': 'BioxelNodes_CubeObjectCutter',
|
||||
'node_description': '',
|
||||
},
|
||||
{
|
||||
'label': 'Sphere Cutter',
|
||||
'icon': 'MESH_UVSPHERE',
|
||||
'node_type': 'BioxelNodes_SphereObjectCutter',
|
||||
'node_description': '',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Utils',
|
||||
'icon': 'MODIFIER',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Join Component',
|
||||
'icon': 'CONSTRAINT_BONE',
|
||||
'node_type': 'BioxelNodes_JoinComponent',
|
||||
'node_description': ''
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
custom_nodes = CustomNodes(
|
||||
menu_items=MENU_ITEMS,
|
||||
nodes_file=Path(__file__).parent / f"assets/Nodes/{NODE_FILE}.blend",
|
||||
class_prefix="BIOXELNODES_MT_NODES",
|
||||
root_label='Bioxel Nodes',
|
||||
root_icon="FILE_VOLUME",
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from . import skimage as ski
|
||||
from . import scipy
|
||||
from .nodes import custom_nodes
|
||||
from .utils import (get_container, get_container_from_selection, get_container_layers,
|
||||
get_layer, get_node_by_type, hide_in_ray, lock_transform, save_vdb)
|
||||
get_layer, get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb)
|
||||
|
||||
|
||||
def get_layer_name(layer):
|
||||
@@ -52,23 +52,22 @@ def set_layer_meta(layer, key: str, value):
|
||||
|
||||
def add_mask_node(container, layer, node_type: str, node_label: str):
|
||||
modifier = container.modifiers[0]
|
||||
node_tree = modifier.node_group
|
||||
container_node_group = modifier.node_group
|
||||
|
||||
# Deselect all nodes first
|
||||
for node in node_tree.nodes:
|
||||
if node.select:
|
||||
node.select = False
|
||||
|
||||
mask_node = custom_nodes.add_node(node_tree.nodes, node_type)
|
||||
mask_node = custom_nodes.add_node(container_node_group, node_type)
|
||||
mask_node.label = node_label
|
||||
mask_node.inputs[0].default_value = layer
|
||||
|
||||
# Connect to output if no output linked
|
||||
output_node = get_node_by_type(node_tree.nodes,
|
||||
output_node = get_nodes_by_type(container_node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_tree.links.new(mask_node.outputs[0],
|
||||
container_node_group.links.new(mask_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(mask_node, output_node, (-300, 0))
|
||||
else:
|
||||
move_node_to_node(mask_node, output_node, (0, -100))
|
||||
|
||||
return mask_node
|
||||
|
||||
@@ -106,6 +105,7 @@ def deep_copy_layer(vdb_path, base_layer, name):
|
||||
# add convert to layer node
|
||||
base_layer_node = base_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
||||
|
||||
dtype_index = base_layer_node.inputs['Data Type'].default_value
|
||||
bioxel_size = base_layer_node.inputs['Bioxel Size'].default_value
|
||||
layer_shape = base_layer_node.inputs['Shape'].default_value
|
||||
layer_origin = base_layer_node.inputs['Origin'].default_value
|
||||
@@ -115,19 +115,22 @@ def deep_copy_layer(vdb_path, base_layer, name):
|
||||
scalar_max = base_layer_node.inputs['Scalar Max'].default_value
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
node_tree = copyed_layer.modifiers[0].node_group
|
||||
node_group = copyed_layer.modifiers[0].node_group
|
||||
|
||||
input_node = get_node_by_type(node_tree.nodes, 'NodeGroupInput')[0]
|
||||
output_node = get_node_by_type(node_tree.nodes, 'NodeGroupOutput')[0]
|
||||
input_node = get_nodes_by_type(node_group, 'NodeGroupInput')[0]
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
|
||||
copyed_layer_node = custom_nodes.add_node(node_tree.nodes,
|
||||
copyed_layer_node = custom_nodes.add_node(node_group,
|
||||
"BioxelNodes__ConvertToLayer")
|
||||
|
||||
node_tree.links.new(input_node.outputs[0], copyed_layer_node.inputs[0])
|
||||
node_tree.links.new(copyed_layer_node.outputs[0], output_node.inputs[0])
|
||||
node_group.links.new(input_node.outputs[0], copyed_layer_node.inputs[0])
|
||||
node_group.links.new(copyed_layer_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
# for compatibility to old vdb
|
||||
copyed_layer_node.inputs['Not Transfromed'].default_value = True
|
||||
copyed_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
|
||||
200000000)
|
||||
copyed_layer_node.inputs['Data Type'].default_value = dtype_index
|
||||
copyed_layer_node.inputs['Bioxel Size'].default_value = bioxel_size
|
||||
copyed_layer_node.inputs['Shape'].default_value = layer_shape
|
||||
copyed_layer_node.inputs['Origin'].default_value = layer_origin
|
||||
@@ -501,6 +504,7 @@ class FillByLabel(bpy.types.Operator):
|
||||
mask = get_volume(label_grids, 0, label_shape)
|
||||
mask = ski.resize(mask,
|
||||
base_shape,
|
||||
preserve_range=True,
|
||||
anti_aliasing=True)
|
||||
mask = scipy.median_filter(mask.astype(np.float32), size=2)
|
||||
|
||||
@@ -564,6 +568,7 @@ class CombineLabels(bpy.types.Operator):
|
||||
label_volume = get_volume(label_grids, 0, label_shape)
|
||||
label_volume = ski.resize(label_volume,
|
||||
base_shape,
|
||||
preserve_range=True,
|
||||
anti_aliasing=True)
|
||||
base_volume = np.maximum(base_volume, label_volume)
|
||||
label_names.append(get_layer_name(label))
|
||||
@@ -586,8 +591,8 @@ class CombineLabels(bpy.types.Operator):
|
||||
|
||||
class ConvertToMesh(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.convert_to_mesh"
|
||||
bl_label = "Convert To Mesh"
|
||||
bl_description = "Convert Bioxel Components To Mesh"
|
||||
bl_label = "Convert Container To Mesh"
|
||||
bl_description = "Convert Bioxel Container To Mesh"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
@@ -615,21 +620,24 @@ class ConvertToMesh(bpy.types.Operator):
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
modifier = mesh.modifiers[0]
|
||||
node_tree = modifier.node_group
|
||||
node_group = modifier.node_group
|
||||
|
||||
output_node = get_node_by_type(node_tree.nodes, 'NodeGroupOutput')[0]
|
||||
to_mesh_node = custom_nodes.add_node(node_tree.nodes,
|
||||
"BioxelNodes_ToMesh")
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
to_mesh_node = custom_nodes.add_node(node_group,
|
||||
"BioxelNodes_PickMesh")
|
||||
|
||||
to_mesh_node.inputs[0].default_value = container
|
||||
node_tree.links.new(to_mesh_node.outputs[0], output_node.inputs[0])
|
||||
node_group.links.new(to_mesh_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
bpy.ops.object.convert(target='MESH')
|
||||
# bpy.ops.constraint.apply(
|
||||
# constraint=mesh.constraints[0].name, owner='OBJECT')
|
||||
bpy.ops.object.modifier_apply(modifier=mesh.modifiers[0].name)
|
||||
|
||||
bpy.context.object.active_material_index = 1
|
||||
bpy.ops.object.material_slot_remove()
|
||||
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
|
||||
self.report({"INFO"}, f"Successfully convert to mesh")
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -681,10 +689,34 @@ class AddCutter():
|
||||
cutter.display_type = 'WIRE'
|
||||
|
||||
modifier = container.modifiers[0]
|
||||
node_tree = modifier.node_group
|
||||
cutter_node = custom_nodes.add_node(node_tree.nodes, node_type)
|
||||
node_group = modifier.node_group
|
||||
cutter_node = custom_nodes.add_node(node_group, node_type)
|
||||
cutter_node.inputs[0].default_value = cutter
|
||||
|
||||
cut_nodes = get_nodes_by_type(node_group,
|
||||
'BioxelNodes_Cut')
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
if len(cut_nodes) == 0:
|
||||
cut_node = custom_nodes.add_node(node_group, 'BioxelNodes_Cut')
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_group.links.new(cut_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(cut_node, output_node, (-300, 0))
|
||||
else:
|
||||
pre_output_node = output_node.inputs[0].links[0].from_node
|
||||
node_group.links.new(pre_output_node.outputs[0],
|
||||
cut_node.inputs[0])
|
||||
node_group.links.new(cut_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_between_nodes(cut_node,
|
||||
[pre_output_node, output_node])
|
||||
|
||||
node_group.links.new(cutter_node.outputs[0],
|
||||
cut_node.inputs[1])
|
||||
|
||||
move_node_to_node(cutter_node, cut_node, (-300, -300))
|
||||
else:
|
||||
move_node_to_node(cutter_node, output_node, (0, -100))
|
||||
bpy.context.view_layer.objects.active = container
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -692,31 +724,31 @@ class AddCutter():
|
||||
|
||||
class AddPlaneCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_plane_cutter"
|
||||
bl_label = "Add Plane Cutter"
|
||||
bl_description = "Add Plane Cutter to Container"
|
||||
bl_label = "Add a Plane Cutter"
|
||||
bl_description = "Add a Plane Cutter to Container"
|
||||
bl_options = {'UNDO'}
|
||||
object_type = "plane"
|
||||
|
||||
|
||||
class AddCylinderCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_cylinder_cutter"
|
||||
bl_label = "Add Cylinder Cutter"
|
||||
bl_description = "Add Cylinder Cutter to Container"
|
||||
bl_label = "Add a Cylinder Cutter"
|
||||
bl_description = "Add a Cylinder Cutter to Container"
|
||||
bl_options = {'UNDO'}
|
||||
object_type = "cylinder"
|
||||
|
||||
|
||||
class AddCubeCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_cube_cutter"
|
||||
bl_label = "Add Cube Cutter"
|
||||
bl_description = "Add Cube Cutter to Container"
|
||||
bl_label = "Add a Cube Cutter"
|
||||
bl_description = "Add a Cube Cutter to Container"
|
||||
bl_options = {'UNDO'}
|
||||
object_type = "cube"
|
||||
|
||||
|
||||
class AddSphereCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_sphere_cutter"
|
||||
bl_label = "Add Sphere Cutter"
|
||||
bl_description = "Add Sphere Cutter to Container"
|
||||
bl_label = "Add a Sphere Cutter"
|
||||
bl_description = "Add a Sphere Cutter to Container"
|
||||
bl_options = {'UNDO'}
|
||||
object_type = "sphere"
|
||||
|
||||
165
bioxelnodes/save.py
Normal file
165
bioxelnodes/save.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from .utils import get_all_layers, get_container, get_container_layers
|
||||
from .nodes import custom_nodes
|
||||
|
||||
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
|
||||
|
||||
|
||||
class ReLinkNodes(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.relink_nodes"
|
||||
bl_label = "Relink Nodes to Addon"
|
||||
bl_description = "Relink all nodes to addon source"
|
||||
|
||||
def execute(self, context):
|
||||
file_name = Path(custom_nodes.nodes_file).name
|
||||
for lib in bpy.data.libraries:
|
||||
lib_path = Path(bpy.path.abspath(lib.filepath)).resolve()
|
||||
lib_name = lib_path.name
|
||||
if lib_name == file_name:
|
||||
if str(lib_path) != custom_nodes.nodes_file:
|
||||
lib.filepath = custom_nodes.nodes_file
|
||||
|
||||
self.report({"INFO"}, f"Successfully relinked.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveAllToShare(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_all_to_share"
|
||||
bl_label = "Save All Staged Data"
|
||||
bl_description = "Save all staged data for sharing"
|
||||
|
||||
layer_dir: bpy.props.StringProperty(
|
||||
name="Layer Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
save_node: bpy.props.BoolProperty(
|
||||
name="Save Node Library File",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
node_file_dir: bpy.props.StringProperty(
|
||||
name="Library Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
files = []
|
||||
for classname in dir(bpy.types):
|
||||
if CLASS_PREFIX in classname:
|
||||
cls = getattr(bpy.types, classname)
|
||||
files.append(cls.nodes_file)
|
||||
files = list(set(files))
|
||||
|
||||
for file in files:
|
||||
file_name = Path(file).name
|
||||
# "//"
|
||||
node_file_dir = bpy.path.abspath(self.node_file_dir)
|
||||
|
||||
output_path: Path = Path(node_file_dir, file_name).resolve()
|
||||
source_path: Path = Path(file).resolve()
|
||||
|
||||
if output_path != source_path:
|
||||
shutil.copy(source_path, output_path)
|
||||
|
||||
for lib in bpy.data.libraries:
|
||||
lib_path = Path(bpy.path.abspath(lib.filepath)).resolve()
|
||||
if lib_path == source_path:
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
lib.filepath = bpy.path.relpath(
|
||||
str(output_path), start=str(blend_path))
|
||||
|
||||
self.report({"INFO"}, f"Successfully saved to {output_path}")
|
||||
|
||||
layers = get_all_layers()
|
||||
for layer in layers:
|
||||
try:
|
||||
save_layer(layer, self.layer_dir)
|
||||
except:
|
||||
self.report(
|
||||
{"WARNING"}, f"Fail to save {layer.name}, skiped")
|
||||
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bpy.data.is_saved
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
panel = layout.box()
|
||||
panel.prop(self, "layer_dir")
|
||||
panel = layout.box()
|
||||
panel.prop(self, "save_node")
|
||||
panel.prop(self, "node_file_dir")
|
||||
layout.label(text="Save your blender file first.")
|
||||
|
||||
|
||||
def save_layer(layer, output_dir):
|
||||
name = layer.name
|
||||
|
||||
# "//"
|
||||
output_dir = bpy.path.abspath(output_dir)
|
||||
source_dir = bpy.path.abspath(layer.data.filepath)
|
||||
|
||||
output_path: Path = Path(output_dir, f"{name}.vdb").resolve()
|
||||
source_path: Path = Path(source_dir).resolve()
|
||||
|
||||
if output_path != source_path:
|
||||
shutil.copy(source_path, output_path)
|
||||
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
layer.data.filepath = bpy.path.relpath(
|
||||
str(output_path), start=str(blend_path))
|
||||
|
||||
|
||||
class SaveLayers(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_layers"
|
||||
bl_label = "Save Container's Layers"
|
||||
bl_description = "Save Bioxel Layers to Directory."
|
||||
|
||||
layer_dir: bpy.props.StringProperty(
|
||||
name="Layer Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
container = get_container(bpy.context.active_object)
|
||||
return True if container else False
|
||||
|
||||
def execute(self, context):
|
||||
container = get_container(bpy.context.active_object)
|
||||
|
||||
if not container:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
for layer in get_container_layers(container):
|
||||
try:
|
||||
save_layer(layer, self.layer_dir)
|
||||
except:
|
||||
self.report(
|
||||
{"WARNING"}, f"Fail to save {layer.name}, skiped")
|
||||
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500)
|
||||
return {'RUNNING_MODAL'}
|
||||
@@ -5,12 +5,32 @@ import pyopenvdb as vdb
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def get_type(cls):
|
||||
return type(cls).__name__
|
||||
def move_node_to_node(node, target_node, offset=(0, 0)):
|
||||
node.location.x = target_node.location.x + offset[0]
|
||||
node.location.y = target_node.location.y + offset[1]
|
||||
|
||||
|
||||
def get_node_by_type(nodes, type_name: str):
|
||||
return [node for node in nodes if get_type(node) == type_name]
|
||||
def move_node_between_nodes(node, target_nodes, offset=(0, 0)):
|
||||
xs = []
|
||||
ys = []
|
||||
for target_node in target_nodes:
|
||||
xs.append(target_node.location.x)
|
||||
ys.append(target_node.location.y)
|
||||
|
||||
node.location.x = sum(xs) / len(xs) + offset[0]
|
||||
node.location.y = sum(ys) / len(ys) + offset[1]
|
||||
|
||||
|
||||
def get_node_type(node):
|
||||
node_type = type(node).__name__
|
||||
if node_type == "GeometryNodeGroup":
|
||||
node_type = node.node_tree.name
|
||||
|
||||
return node_type
|
||||
|
||||
|
||||
def get_nodes_by_type(node_group, type_name: str):
|
||||
return [node for node in node_group.nodes if get_node_type(node) == type_name]
|
||||
|
||||
|
||||
def show_message(message="", title="Message Box", icon='INFO'):
|
||||
|
||||
@@ -10,11 +10,6 @@ auto_load.init()
|
||||
def register():
|
||||
auto_load.register()
|
||||
menus.add()
|
||||
bpy.types.Scene.bioxel_layer_dir = bpy.props.StringProperty(
|
||||
name="Layer Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
@@ -3,7 +3,7 @@ schema_version = "1.0.0"
|
||||
# Example of manifest file for a Blender extension
|
||||
# Change the values according to your extension
|
||||
id = "bioxelnodes"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
name = "Bioxel Nodes"
|
||||
tagline = "For scientific volumetric data visualization in Blender"
|
||||
maintainer = "Ma Nan <icrdr2010@outlook.com>"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "bioxelnodes"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
description = ""
|
||||
authors = ["Ma Nan <icrdr2010@outlook.com>"]
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user