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:
2024-07-09 15:20:26 +08:00
parent f90bf5c532
commit 99ce5c28d0
16 changed files with 576 additions and 623 deletions

View File

@@ -7,11 +7,11 @@
![GitHub Release](https://img.shields.io/github/v/release/OmooLab/BioxelNodes?style=for-the-badge) ![GitHub Release](https://img.shields.io/github/v/release/OmooLab/BioxelNodes?style=for-the-badge)
![GitHub Repo stars](https://img.shields.io/github/stars/OmooLab/BioxelNodes?style=for-the-badge) ![GitHub Repo stars](https://img.shields.io/github/stars/OmooLab/BioxelNodes?style=for-the-badge)
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 ## 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: 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 ## Known Limitations
- Cycles CPUs only, Cycles GPUs (Optix), EEVEE (partial support)
- Sections cannot be generated (will be supported soon) - Sections cannot be generated (will be supported soon)
- Time sequence volume not supported (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 ### For project files that need to be archived
2. Add the new version and restart Blender.
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 ## 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. Bioxel Nodes is designed for Cycles Render. However, it does support eevee render partially.Also, there are some limitations:
Also, there are some limitations:
1. Only one cutter supported. 1. Only one cutter supported.
2. You cannot use "Color Ramp" over 2 colors. 2. EEVEE Render result is not that great as Cycles does.
3. 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.

View File

@@ -8,8 +8,8 @@ bl_info = {
"name": "Bioxel Nodes", "name": "Bioxel Nodes",
"author": "Ma Nan", "author": "Ma Nan",
"description": "", "description": "",
"blender": (4, 0, 0), "blender": (4, 1, 0),
"version": (0, 2, 4), "version": (0, 2, 5),
"location": "File -> Import", "location": "File -> Import",
"warning": "", "warning": "",
"category": "Node" "category": "Node"
@@ -21,11 +21,6 @@ auto_load.init()
def register(): def register():
auto_load.register() auto_load.register()
menus.add() menus.add()
bpy.types.Scene.bioxel_layer_dir = bpy.props.StringProperty(
name="Layer Directory",
subtype='DIR_PATH',
default="//"
)
def unregister(): def unregister():

View File

@@ -4,62 +4,12 @@ from pathlib import Path
from .nodes import AddCustomNode 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(): class CustomNodes():
def __init__( def __init__(
self, self,
menu_items, menu_items,
nodes_file, nodes_file,
class_prefix="CUSTOMNODES_MT_NODES",
root_label='CustomNodes', root_label='CustomNodes',
root_icon='NONE' root_icon='NONE'
) -> None: ) -> None:
@@ -68,9 +18,10 @@ class CustomNodes():
self.menu_items = menu_items self.menu_items = menu_items
self.nodes_file = str(Path(nodes_file).resolve()) self.nodes_file = str(Path(nodes_file).resolve())
self.class_prefix = class_prefix
self.root_label = root_label self.root_label = root_label
self.root_icon = root_icon self.root_icon = root_icon
self.class_prefix = class_prefix
menu_classes = [] menu_classes = []
self._create_menu_class( self._create_menu_class(
@@ -80,7 +31,7 @@ class CustomNodes():
) )
self.menu_classes = menu_classes 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): def add_node_menu(self, context):
if ('GeometryNodeTree' == bpy.context.area.spaces[0].tree_type): 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): def _create_menu_class(self, menu_classes, items, label='CustomNodes', icon=0, idname_namespace=None):
nodes_file = self.nodes_file 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()}" idname = f"{idname_namespace}_{label.replace(' ', '').upper()}"
# create submenu class if item is menu. # create submenu class if item is menu.
@@ -161,7 +112,7 @@ class CustomNodes():
self._find_item(found_items, self.menu_items, node_type) self._find_item(found_items, self.menu_items, node_type)
return found_items[0] if len(found_items) > 0 else None 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) item = self.find_item(node_type)
op = AddCustomNode() op = AddCustomNode()
op.nodes_file = self.nodes_file op.nodes_file = self.nodes_file
@@ -174,7 +125,7 @@ class CustomNodes():
op.node_label = "" op.node_label = ""
op.node_link = True op.node_link = True
op.node_callback = "" op.node_callback = ""
return op.add_node(node_tree) return op.add_node(node_group)
def register(self): def register(self):
for cls in self.menu_classes: for cls in self.menu_classes:

View File

@@ -85,9 +85,14 @@ class AddCustomNode():
except: 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) 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) self.assign_node_tree(node)
return node return node

View File

@@ -13,7 +13,7 @@ from . import skimage as ski
from .nodes import custom_nodes from .nodes import custom_nodes
from .props import BIOXELNODES_Series from .props import BIOXELNODES_Series
from .utils import (calc_bbox_verts, get_all_layers, get_container_from_selection, get_layer, get_text_index_str, 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: try:
import SimpleITK as sitk import SimpleITK as sitk
@@ -233,6 +233,8 @@ class ImportVolumeDataDialog(bpy.types.Operator):
meta['spacing'][2] / orig_spacing[2] * bioxel_size meta['spacing'][2] / orig_spacing[2] * bioxel_size
) )
dtype_index = volume.dtype.num
layer_shape = get_layer_shape( layer_shape = get_layer_shape(
bioxel_size, meta['shape'], orig_spacing) bioxel_size, meta['shape'], orig_spacing)
@@ -285,10 +287,9 @@ class ImportVolumeDataDialog(bpy.types.Operator):
) )
container = bpy.context.active_object 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): for index, vert in enumerate(container.data.vertices):
bbox_transform = transfrom vert.co = transfrom @ mathutils.Vector(bbox_verts[index])
vert.co = bbox_transform @ mathutils.Vector(bbox_verts[index])
container.matrix_world = mat_ras2blender @ mat_scene_scale container.matrix_world = mat_ras2blender @ mat_scene_scale
container.name = container_name container.name = container_name
@@ -297,14 +298,14 @@ class ImportVolumeDataDialog(bpy.types.Operator):
container['bioxel_container'] = True container['bioxel_container'] = True
container['scene_scale'] = scene_scale container['scene_scale'] = scene_scale
bpy.ops.node.new_geometry_nodes_modifier() bpy.ops.node.new_geometry_nodes_modifier()
container_node_tree = container.modifiers[0].node_group container_node_group = container.modifiers[0].node_group
input_node = get_node_by_type(container_node_tree.nodes, input_node = get_nodes_by_type(container_node_group,
'NodeGroupInput')[0] '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: else:
container = bpy.data.objects[self.container] 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 preferences = context.preferences.addons[__package__].preferences
loc, rot, sca = transfrom.decompose() loc, rot, sca = transfrom.decompose()
@@ -316,7 +317,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
grid = vdb.FloatGrid() grid = vdb.FloatGrid()
volume = volume.copy().astype(np.float32) volume = volume.copy().astype(np.float32)
grid.copyFromArray(volume) grid.copyFromArray(volume)
grid.transform = vdb.createLinearTransform(transfrom.transposed()) # grid.transform = vdb.createLinearTransform(transfrom.transposed())
grid.name = layer_type grid.name = layer_type
vdb_path = save_vdb([grid], context) vdb_path = save_vdb([grid], context)
@@ -353,26 +354,31 @@ class ImportVolumeDataDialog(bpy.types.Operator):
print(f"Creating layer ...") print(f"Creating layer ...")
bpy.ops.node.new_geometry_nodes_modifier() bpy.ops.node.new_geometry_nodes_modifier()
node_tree = layer.modifiers[0].node_group node_group = layer.modifiers[0].node_group
nodes = node_tree.nodes
links = node_tree.links
input_node = get_node_by_type(nodes, 'NodeGroupInput')[0] input_node = get_nodes_by_type(node_group, 'NodeGroupInput')[0]
output_node = get_node_by_type(nodes, 'NodeGroupOutput')[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") "BioxelNodes__ConvertToLayer")
links.new(input_node.outputs[0], to_layer_node.inputs[0]) node_group.links.new(input_node.outputs[0],
links.new(to_layer_node.outputs[0], output_node.inputs[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, to_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
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['Bioxel Size'].default_value = bioxel_size
to_layer_node.inputs['Shape'].default_value = layer_shape to_layer_node.inputs['Shape'].default_value = layer_shape
to_layer_node.inputs['Origin'].default_value = layer_origin to_layer_node.inputs['Origin'].default_value = layer_origin
to_layer_node.inputs['Rotation'].default_value = layer_rotation to_layer_node.inputs['Rotation'].default_value = layer_rotation
move_node_between_nodes(to_layer_node, [input_node, output_node])
return layer return layer
if self.read_as == "labels": if self.read_as == "labels":
@@ -386,29 +392,29 @@ class ImportVolumeDataDialog(bpy.types.Operator):
for i in range(orig_max): for i in range(orig_max):
label = volume == np.full_like(volume, i+1) label = volume == np.full_like(volume, i+1)
print(f"Resampling...") print(f"Resampling...")
label = ski.resize(label.astype(np.float32), label = ski.resize(label,
layer_shape, layer_shape,
anti_aliasing=True) preserve_range=True,
anti_aliasing=False)
layer = create_layer(volume=label, layer = create_layer(volume=label,
layer_name=f"{container_name}_{layer_name}_{i+1}", layer_name=f"{container_name}_{layer_name}_{i+1}",
layer_type="label") layer_type="label")
# Deselect all nodes first mask_node = custom_nodes.add_node(container_node_group,
for node in container_node_tree.nodes:
if node.select:
node.select = False
mask_node = custom_nodes.add_node(container_node_tree.nodes,
'BioxelNodes_MaskByLabel') 'BioxelNodes_MaskByLabel')
mask_node.label = f"{layer_name}_{i+1}" mask_node.label = f"{layer_name}_{i+1}"
mask_node.inputs[0].default_value = layer mask_node.inputs[0].default_value = layer
# Connect to output if no output linked # 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] 'NodeGroupOutput')[0]
if len(output_node.inputs[0].links) == 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]) 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: else:
if volume.ndim == 4: if volume.ndim == 4:
@@ -442,27 +448,27 @@ class ImportVolumeDataDialog(bpy.types.Operator):
layer_name=f"{container_name}_{layer_name}", layer_name=f"{container_name}_{layer_name}",
layer_type="scalar") layer_type="scalar")
layer_node_tree = layer.modifiers[0].node_group layer_node_group = layer.modifiers[0].node_group
to_layer_node = layer_node_tree.nodes['BioxelNodes__ConvertToLayer'] to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer']
to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset 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 Max'].default_value = orig_max
to_layer_node.inputs['Scalar Min'].default_value = orig_min to_layer_node.inputs['Scalar Min'].default_value = orig_min
# Deselect all nodes first mask_node = custom_nodes.add_node(container_node_group,
for node in container_node_tree.nodes:
if node.select:
node.select = False
mask_node = custom_nodes.add_node(container_node_tree.nodes,
'BioxelNodes_MaskByThreshold') 'BioxelNodes_MaskByThreshold')
mask_node.label = layer_name mask_node.label = layer_name
mask_node.inputs[0].default_value = layer mask_node.inputs[0].default_value = layer
# Connect to output if no output linked # 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] 'NodeGroupOutput')[0]
if len(output_node.inputs[0].links) == 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]) 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 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: if preferences.do_change_render_setting and is_first_import:
bpy.context.scene.render.engine = 'CYCLES' bpy.context.scene.render.engine = 'CYCLES'
try: try:
bpy.context.scene.cycles.shading_system = True
bpy.context.scene.cycles.volume_bounces = 12 bpy.context.scene.cycles.volume_bounces = 12
bpy.context.scene.cycles.transparent_max_bounces = 16 bpy.context.scene.cycles.transparent_max_bounces = 16
bpy.context.scene.cycles.volume_preview_step_rate = 10 bpy.context.scene.cycles.volume_preview_step_rate = 10
@@ -489,8 +496,8 @@ class ImportVolumeDataDialog(bpy.types.Operator):
volume_dtype = "Label" volume_dtype = "Label"
elif self.read_as == "scalar": elif self.read_as == "scalar":
volume_dtype = "Scalar" volume_dtype = "Scalar"
title = f"Import '{volume_dtype}' Layer (Add to Container: {self.container})" \ title = f"Import as **{volume_dtype}** (Add to Container: {self.container})" \
if self.container != "" else f"Import '{volume_dtype}' Layer (Init a Container)" if self.container != "" else f"Import as **{volume_dtype}** (Init a Container)"
context.window_manager.invoke_props_dialog(self, context.window_manager.invoke_props_dialog(self,
width=500, width=500,
title=title) 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" layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m"
layout = self.layout layout = self.layout
panel = layout.box()
if self.container == "": if self.container == "":
panel.prop(self, "container_name") layout.prop(self, "container_name")
panel.prop(self, "layer_name") layout.prop(self, "layer_name")
panel = layout.box() panel = layout.box()
panel.prop(self, "bioxel_size") panel.prop(self, "bioxel_size")
@@ -575,7 +581,16 @@ class ParseVolumeData(bpy.types.Operator):
print("Collecting Meta Data...") print("Collecting Meta Data...")
volume, meta = parse_volume_data(self.filepath) 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_shape = meta['shape']
orig_spacing = meta['spacing'] orig_spacing = meta['spacing']
@@ -601,7 +616,7 @@ class ParseVolumeData(bpy.types.Operator):
orig_spacing=orig_spacing, orig_spacing=orig_spacing,
bioxel_size=bioxel_size, bioxel_size=bioxel_size,
series_id=self.series_id or "", series_id=self.series_id or "",
do_orient=do_orient, # do_orient=do_orient,
container=self.container, container=self.container,
read_as=self.read_as, read_as=self.read_as,
scene_scale=scene_scale scene_scale=scene_scale
@@ -619,7 +634,7 @@ class ParseVolumeData(bpy.types.Operator):
if not self.filepath and not self.directory: if not self.filepath and not self.directory:
return {'CANCELLED'} 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...') 'Please be patient...')
if get_ext(self.filepath) == '.dcm': if get_ext(self.filepath) == '.dcm':
@@ -675,7 +690,8 @@ class ImportVolumeData():
bpy.ops.bioxelnodes.parse_volume_data( bpy.ops.bioxelnodes.parse_volume_data(
'INVOKE_DEFAULT', 'INVOKE_DEFAULT',
filepath=self.filepath, filepath=self.filepath,
directory=self.directory directory=self.directory,
read_as=self.read_as
) )
return {'FINISHED'} return {'FINISHED'}
@@ -745,7 +761,7 @@ class AddVolumeData(bpy.types.Operator):
class ExportVolumeData(bpy.types.Operator): class ExportVolumeData(bpy.types.Operator):
bl_idname = "bioxelnodes.export_volume_data" bl_idname = "bioxelnodes.export_volume_data"
bl_label = "Export Layer" bl_label = "Export Bioxel as VDB"
bl_description = "Export Bioxel Layer as VDB" bl_description = "Export Bioxel Layer as VDB"
bl_options = {'UNDO'} bl_options = {'UNDO'}

View File

@@ -1,8 +1,9 @@
import bpy import bpy
from .operators import (AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels, from .operators import (AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels,
ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange) ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange)
from .io import ExportVolumeData, ImportAsLabelLayer, ImportAsScalarLayer, ImportVolumeData, AddVolumeData from .io import ExportVolumeData, ImportAsLabelLayer, ImportAsScalarLayer, ImportVolumeData, AddVolumeData
from .misc import SaveLayers from .save import ReLinkNodes, SaveLayers, SaveAllToShare
class ModifyLayer(bpy.types.Menu): class ModifyLayer(bpy.types.Menu):
@@ -20,7 +21,7 @@ class ModifyLayer(bpy.types.Menu):
class AddCutterMenu(bpy.types.Menu): class AddCutterMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_CUTTERS" bl_idname = "BIOXELNODES_MT_CUTTERS"
bl_label = "Add Cutter" bl_label = "Add a Cutter to Container"
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@@ -32,7 +33,7 @@ class AddCutterMenu(bpy.types.Menu):
class ImportLayerMenu(bpy.types.Menu): class ImportLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_LAYERS" bl_idname = "BIOXELNODES_MT_LAYERS"
bl_label = "Import Layer" bl_label = "Import Volume Data as Bioxel"
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@@ -68,28 +69,28 @@ class BioxelNodesOutlinerMenu(bpy.types.Menu):
layout.operator(SaveLayers.bl_idname) layout.operator(SaveLayers.bl_idname)
# def TOPBAR_FILE_IMPORT(self, context): def TOPBAR_FILE_IMPORT(self, context):
# layout = self.layout layout = self.layout
# layout.separator() layout.separator()
# layout.operator(ImportVolumeData.bl_idname) layout.menu(ImportLayerMenu.bl_idname)
# def TOPBAR_FILE_EXPORT(self, context): def TOPBAR_FILE_EXPORT(self, context):
# layout = self.layout layout = self.layout
# layout.separator() layout.separator()
# layout.operator(ExportVolumeData.bl_idname) layout.operator(ExportVolumeData.bl_idname)
def VIEW3D_OBJECT(self, context): def VIEW3D_OBJECT(self, context):
layout = self.layout layout = self.layout
layout.menu(BioxelNodesView3DMenu.bl_idname)
layout.separator() layout.separator()
layout.menu(BioxelNodesView3DMenu.bl_idname, icon="FILE_VOLUME")
def OUTLINER_OBJECT(self, context): def OUTLINER_OBJECT(self, context):
layout = self.layout layout = self.layout
layout.menu(BioxelNodesOutlinerMenu.bl_idname)
layout.separator() layout.separator()
layout.menu(BioxelNodesOutlinerMenu.bl_idname, icon="FILE_VOLUME")
class BioxelNodesTopbarMenu(bpy.types.Menu): class BioxelNodesTopbarMenu(bpy.types.Menu):
@@ -104,7 +105,8 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
layout.menu(AddCutterMenu.bl_idname) layout.menu(AddCutterMenu.bl_idname)
layout.operator(ConvertToMesh.bl_idname) layout.operator(ConvertToMesh.bl_idname)
layout.separator() layout.separator()
layout.operator(SaveLayers.bl_idname) layout.operator(SaveAllToShare.bl_idname)
layout.operator(ReLinkNodes.bl_idname)
def TOPBAR(self, context): def TOPBAR(self, context):
@@ -113,16 +115,16 @@ def TOPBAR(self, context):
def add(): def add():
# bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT) bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT)
# bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT) bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT)
bpy.types.OUTLINER_MT_object.prepend(OUTLINER_OBJECT) bpy.types.OUTLINER_MT_object.append(OUTLINER_OBJECT)
bpy.types.VIEW3D_MT_object_context_menu.prepend(VIEW3D_OBJECT) bpy.types.VIEW3D_MT_object_context_menu.append(VIEW3D_OBJECT)
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR) bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR)
def remove(): def remove():
# bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT) 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_export.remove(TOPBAR_FILE_EXPORT)
bpy.types.OUTLINER_MT_object.remove(OUTLINER_OBJECT) bpy.types.OUTLINER_MT_object.remove(OUTLINER_OBJECT)
bpy.types.VIEW3D_MT_object_context_menu.remove(VIEW3D_OBJECT) bpy.types.VIEW3D_MT_object_context_menu.remove(VIEW3D_OBJECT)
bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR) bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR)

View File

@@ -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)

View File

@@ -56,10 +56,9 @@ import bpy
# return callback_str # 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', 'label': 'Methods',
'icon': 'OUTLINER_DATA_VOLUME', 'icon': 'OUTLINER_DATA_VOLUME',
@@ -127,7 +126,13 @@ if bpy.app.version >= (4, 1, 0):
{ {
'label': 'Color Presets', 'label': 'Color Presets',
'icon': 'COLOR', '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': '' 'node_description': ''
}, },
"separator", "separator",
@@ -206,28 +211,28 @@ if bpy.app.version >= (4, 1, 0):
}, },
"separator", "separator",
{ {
'label': 'To Mesh', 'label': 'Pick Mesh',
'icon': 'OUTLINER_OB_MESH', 'icon': 'OUTLINER_OB_MESH',
'node_type': 'BioxelNodes_ToMesh', 'node_type': 'BioxelNodes_PickMesh',
'node_description': '' 'node_description': ''
}, },
{ {
'label': 'To Volume', 'label': 'Pick Volume',
'icon': 'OUTLINER_OB_VOLUME', 'icon': 'OUTLINER_OB_VOLUME',
'node_type': 'BioxelNodes_ToVolume', 'node_type': 'BioxelNodes_PickVolume',
'node_description': '' 'node_description': ''
}, },
{ {
'label': 'To Bbox Wire', 'label': 'Pick Bbox Wire',
'icon': 'MESH_CUBE', 'icon': 'MESH_CUBE',
'node_type': 'BioxelNodes_ToBboxWire', 'node_type': 'BioxelNodes_PickBboxWire',
'node_description': '' 'node_description': ''
}, },
"separator", "separator",
{ {
'label': 'Inflate', 'label': 'Inflate',
'icon': 'OUTLINER_OB_META', 'icon': 'OUTLINER_OB_META',
'node_type': 'META_DATA', 'node_type': 'BioxelNodes_M_Inflate',
'node_description': '' '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( custom_nodes = CustomNodes(
menu_items=MENU_ITEMS, menu_items=MENU_ITEMS,
nodes_file=Path(__file__).parent / f"assets/Nodes/{NODE_FILE}.blend", nodes_file=Path(__file__).parent / f"assets/Nodes/{NODE_FILE}.blend",
class_prefix="BIOXELNODES_MT_NODES",
root_label='Bioxel Nodes', root_label='Bioxel Nodes',
root_icon="FILE_VOLUME", root_icon="FILE_VOLUME",
) )

View File

@@ -8,7 +8,7 @@ from . import skimage as ski
from . import scipy from . import scipy
from .nodes import custom_nodes from .nodes import custom_nodes
from .utils import (get_container, get_container_from_selection, get_container_layers, 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): 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): def add_mask_node(container, layer, node_type: str, node_label: str):
modifier = container.modifiers[0] modifier = container.modifiers[0]
node_tree = modifier.node_group container_node_group = modifier.node_group
# Deselect all nodes first mask_node = custom_nodes.add_node(container_node_group, node_type)
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.label = node_label mask_node.label = node_label
mask_node.inputs[0].default_value = layer mask_node.inputs[0].default_value = layer
# Connect to output if no output linked # 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] 'NodeGroupOutput')[0]
if len(output_node.inputs[0].links) == 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]) 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 return mask_node
@@ -106,6 +105,7 @@ def deep_copy_layer(vdb_path, base_layer, name):
# add convert to layer node # add convert to layer node
base_layer_node = base_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer'] 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 bioxel_size = base_layer_node.inputs['Bioxel Size'].default_value
layer_shape = base_layer_node.inputs['Shape'].default_value layer_shape = base_layer_node.inputs['Shape'].default_value
layer_origin = base_layer_node.inputs['Origin'].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 scalar_max = base_layer_node.inputs['Scalar Max'].default_value
bpy.ops.node.new_geometry_nodes_modifier() 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] input_node = get_nodes_by_type(node_group, 'NodeGroupInput')[0]
output_node = get_node_by_type(node_tree.nodes, 'NodeGroupOutput')[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") "BioxelNodes__ConvertToLayer")
node_tree.links.new(input_node.outputs[0], copyed_layer_node.inputs[0]) node_group.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(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, copyed_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
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['Bioxel Size'].default_value = bioxel_size
copyed_layer_node.inputs['Shape'].default_value = layer_shape copyed_layer_node.inputs['Shape'].default_value = layer_shape
copyed_layer_node.inputs['Origin'].default_value = layer_origin 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 = get_volume(label_grids, 0, label_shape)
mask = ski.resize(mask, mask = ski.resize(mask,
base_shape, base_shape,
preserve_range=True,
anti_aliasing=True) anti_aliasing=True)
mask = scipy.median_filter(mask.astype(np.float32), size=2) 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 = get_volume(label_grids, 0, label_shape)
label_volume = ski.resize(label_volume, label_volume = ski.resize(label_volume,
base_shape, base_shape,
preserve_range=True,
anti_aliasing=True) anti_aliasing=True)
base_volume = np.maximum(base_volume, label_volume) base_volume = np.maximum(base_volume, label_volume)
label_names.append(get_layer_name(label)) label_names.append(get_layer_name(label))
@@ -586,8 +591,8 @@ class CombineLabels(bpy.types.Operator):
class ConvertToMesh(bpy.types.Operator): class ConvertToMesh(bpy.types.Operator):
bl_idname = "bioxelnodes.convert_to_mesh" bl_idname = "bioxelnodes.convert_to_mesh"
bl_label = "Convert To Mesh" bl_label = "Convert Container To Mesh"
bl_description = "Convert Bioxel Components To Mesh" bl_description = "Convert Bioxel Container To Mesh"
bl_options = {'UNDO'} bl_options = {'UNDO'}
@classmethod @classmethod
@@ -615,21 +620,24 @@ class ConvertToMesh(bpy.types.Operator):
bpy.ops.node.new_geometry_nodes_modifier() bpy.ops.node.new_geometry_nodes_modifier()
modifier = mesh.modifiers[0] 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] output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
to_mesh_node = custom_nodes.add_node(node_tree.nodes, to_mesh_node = custom_nodes.add_node(node_group,
"BioxelNodes_ToMesh") "BioxelNodes_PickMesh")
to_mesh_node.inputs[0].default_value = container 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( # bpy.ops.constraint.apply(
# constraint=mesh.constraints[0].name, owner='OBJECT') # 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.context.object.active_material_index = 1
bpy.ops.object.material_slot_remove() bpy.ops.object.material_slot_remove()
bpy.context.view_layer.objects.active = mesh
self.report({"INFO"}, f"Successfully convert to mesh") self.report({"INFO"}, f"Successfully convert to mesh")
return {'FINISHED'} return {'FINISHED'}
@@ -681,10 +689,34 @@ class AddCutter():
cutter.display_type = 'WIRE' cutter.display_type = 'WIRE'
modifier = container.modifiers[0] modifier = container.modifiers[0]
node_tree = modifier.node_group node_group = modifier.node_group
cutter_node = custom_nodes.add_node(node_tree.nodes, node_type) cutter_node = custom_nodes.add_node(node_group, node_type)
cutter_node.inputs[0].default_value = cutter 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 bpy.context.view_layer.objects.active = container
return {'FINISHED'} return {'FINISHED'}
@@ -692,31 +724,31 @@ class AddCutter():
class AddPlaneCutter(bpy.types.Operator, AddCutter): class AddPlaneCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_plane_cutter" bl_idname = "bioxelnodes.add_plane_cutter"
bl_label = "Add Plane Cutter" bl_label = "Add a Plane Cutter"
bl_description = "Add Plane Cutter to Container" bl_description = "Add a Plane Cutter to Container"
bl_options = {'UNDO'} bl_options = {'UNDO'}
object_type = "plane" object_type = "plane"
class AddCylinderCutter(bpy.types.Operator, AddCutter): class AddCylinderCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_cylinder_cutter" bl_idname = "bioxelnodes.add_cylinder_cutter"
bl_label = "Add Cylinder Cutter" bl_label = "Add a Cylinder Cutter"
bl_description = "Add Cylinder Cutter to Container" bl_description = "Add a Cylinder Cutter to Container"
bl_options = {'UNDO'} bl_options = {'UNDO'}
object_type = "cylinder" object_type = "cylinder"
class AddCubeCutter(bpy.types.Operator, AddCutter): class AddCubeCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_cube_cutter" bl_idname = "bioxelnodes.add_cube_cutter"
bl_label = "Add Cube Cutter" bl_label = "Add a Cube Cutter"
bl_description = "Add Cube Cutter to Container" bl_description = "Add a Cube Cutter to Container"
bl_options = {'UNDO'} bl_options = {'UNDO'}
object_type = "cube" object_type = "cube"
class AddSphereCutter(bpy.types.Operator, AddCutter): class AddSphereCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_sphere_cutter" bl_idname = "bioxelnodes.add_sphere_cutter"
bl_label = "Add Sphere Cutter" bl_label = "Add a Sphere Cutter"
bl_description = "Add Sphere Cutter to Container" bl_description = "Add a Sphere Cutter to Container"
bl_options = {'UNDO'} bl_options = {'UNDO'}
object_type = "sphere" object_type = "sphere"

165
bioxelnodes/save.py Normal file
View 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'}

View File

@@ -5,12 +5,32 @@ import pyopenvdb as vdb
from uuid import uuid4 from uuid import uuid4
def get_type(cls): def move_node_to_node(node, target_node, offset=(0, 0)):
return type(cls).__name__ 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): def move_node_between_nodes(node, target_nodes, offset=(0, 0)):
return [node for node in nodes if get_type(node) == type_name] 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'): def show_message(message="", title="Message Box", icon='INFO'):

View File

@@ -10,11 +10,6 @@ auto_load.init()
def register(): def register():
auto_load.register() auto_load.register()
menus.add() menus.add()
bpy.types.Scene.bioxel_layer_dir = bpy.props.StringProperty(
name="Layer Directory",
subtype='DIR_PATH',
default="//"
)
def unregister(): def unregister():

View File

@@ -3,7 +3,7 @@ schema_version = "1.0.0"
# Example of manifest file for a Blender extension # Example of manifest file for a Blender extension
# Change the values according to your extension # Change the values according to your extension
id = "bioxelnodes" id = "bioxelnodes"
version = "0.2.4" version = "0.2.5"
name = "Bioxel Nodes" name = "Bioxel Nodes"
tagline = "For scientific volumetric data visualization in Blender" tagline = "For scientific volumetric data visualization in Blender"
maintainer = "Ma Nan <icrdr2010@outlook.com>" maintainer = "Ma Nan <icrdr2010@outlook.com>"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "bioxelnodes" name = "bioxelnodes"
version = "0.2.4" version = "0.2.5"
description = "" description = ""
authors = ["Ma Nan <icrdr2010@outlook.com>"] authors = ["Ma Nan <icrdr2010@outlook.com>"]
license = "MIT" license = "MIT"