feat: add save bioxels function

This commit is contained in:
2024-04-24 14:54:22 +08:00
parent 15b24b61b5
commit a579b24f9a
11 changed files with 132 additions and 33 deletions

View File

@@ -9,12 +9,20 @@
Bioxel Nodes is a Blender add-on for scientific volumetric data visualization. It using Blender's powerful Geometry Nodes | Cycles to process and render volumetric data. Bioxel Nodes is a Blender add-on for scientific volumetric data visualization. It using Blender's powerful Geometry Nodes | Cycles to process and render volumetric data.
# About
Before us, there have been many tutorials and add-ons for importing volumetric data into Blender. However, we found that there were many details that were not addressed in place, some scientific facts were ignored, and the volume rendering was not pretty enough. With Bioxel Nodes, you can easily import the volumetric data into Blender, and more importantly, it can quickly make a beautiful realistic rendering of it. Before us, there have been many tutorials and add-ons for importing volumetric data into Blender. However, we found that there were many details that were not addressed in place, some scientific facts were ignored, and the volume rendering was not pretty enough. With Bioxel Nodes, you can easily import the volumetric data into Blender, and more importantly, it can quickly make a beautiful realistic rendering of it.
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:
![gallery](docs/images/gallery.png) ![gallery](docs/images/gallery.png)
### Known limitations
- Cannot read DICOM with multiple series.
- Sections cannot be generated
- Only sequence files are supported, packed volume data formats are not supported yet (will be supported soon)
# Getting Started # Getting Started
Currently only support Blender 4.1 or above, make sure you have the correct version of Blender. Currently only support Blender 4.1 or above, make sure you have the correct version of Blender.
@@ -76,12 +84,6 @@ You can add a "Bake" node between segmentation node and shader node, if the reco
![bake node](docs/images/bake_node.png) ![bake node](docs/images/bake_node.png)
# Known limitations
- Cannot read DICOM with multiple series.
- Sections cannot be generated
- Only sequence files are supported, packed volume data formats are not supported yet (will be supported soon)
# Future Features # Future Features
- Support more volumetric data formats (.nii, .map, .txm...) - Support more volumetric data formats (.nii, .map, .txm...)

View File

@@ -18,6 +18,12 @@ bl_info = {
auto_load.init() auto_load.init()
bpy.types.Scene.bioxels_dir = bpy.props.StringProperty(
name="Bioxels Directory",
subtype='DIR_PATH',
default="//"
)
def register(): def register():
auto_load.register() auto_load.register()

View File

@@ -1,5 +1,5 @@
import bpy import bpy
from .utils import get_bioxels_obj from .utils import get_bioxels_obj, get_node_by_type
class ConvertToMesh(bpy.types.Operator): class ConvertToMesh(bpy.types.Operator):
@@ -8,6 +8,15 @@ class ConvertToMesh(bpy.types.Operator):
bl_description = "Convert Bioxels To Mesh." bl_description = "Convert Bioxels To Mesh."
bl_options = {'UNDO'} bl_options = {'UNDO'}
@classmethod
def poll(cls, context):
bioxels_obj = None
for obj in bpy.context.selected_objects:
bioxels_obj = get_bioxels_obj(obj)
break
return True if bioxels_obj else False
def execute(self, context): def execute(self, context):
bioxels_objs = [] bioxels_objs = []
@@ -31,7 +40,7 @@ class ConvertToMesh(bpy.types.Operator):
nodes = modifier.node_group.nodes nodes = modifier.node_group.nodes
links = modifier.node_group.links links = modifier.node_group.links
output_node = nodes.get("Group Output") output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0]
object_node = nodes.new("GeometryNodeObjectInfo") object_node = nodes.new("GeometryNodeObjectInfo")
realize_nodes = nodes.new("GeometryNodeRealizeInstances") realize_nodes = nodes.new("GeometryNodeRealizeInstances")
object_node.inputs[0].default_value = bioxels_obj object_node.inputs[0].default_value = bioxels_obj
@@ -43,4 +52,6 @@ class ConvertToMesh(bpy.types.Operator):
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()
self.report({"INFO"}, f"Successfully convert to mesh")
return {'FINISHED'} return {'FINISHED'}

View File

@@ -12,10 +12,10 @@ class CustomNodes():
root_icon='NONE' root_icon='NONE'
) -> None: ) -> None:
if not Path(nodes_file).is_file(): if not Path(nodes_file).is_file():
raise FileNotFoundError(Path(nodes_file).resolve().as_posix()) raise FileNotFoundError(str(Path(nodes_file).resolve()))
self.menu_items = menu_items self.menu_items = menu_items
self.nodes_file = Path(nodes_file).resolve().as_posix() self.nodes_file = str(Path(nodes_file).resolve())
self.root_label = root_label self.root_label = root_label
self.root_icon = root_icon self.root_icon = root_icon
@@ -28,7 +28,7 @@ class CustomNodes():
) )
self.menu_classes = menu_classes self.menu_classes = menu_classes
idname = f"CUSTOMNODES_MT_NODES__{root_label.replace(' ', '').upper()}" idname = f"CUSTOMNODES_MT_NODES_{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):
@@ -40,7 +40,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 "CUSTOMNODES_MT_NODES"
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.

View File

@@ -231,7 +231,7 @@ class PackageInstaller():
""" """
# path to python.exe # path to python.exe
python_exe = Path(sys.executable).resolve().as_posix() python_exe = str(Path(sys.executable).resolve())
# build the command list # build the command list
cmd_list = [python_exe] + cmd_list cmd_list = [python_exe] + cmd_list

View File

@@ -14,13 +14,13 @@ class ExternalPackagePreferences():
requirements_dir: bpy.props.StringProperty( requirements_dir: bpy.props.StringProperty(
name="requirements.txt Directory", name="requirements.txt Directory",
subtype='DIR_PATH', subtype='DIR_PATH',
default=Path(__file__).parent.as_posix() default=str(Path(__file__).parent)
) # type: ignore ) # type: ignore
log_dir: bpy.props.StringProperty( log_dir: bpy.props.StringProperty(
name="Python Log Directory", name="Python Log Directory",
subtype='DIR_PATH', subtype='DIR_PATH',
default=Path(Path.home(), '.externalpackage', 'logs').as_posix() default=str(Path(Path.home(), '.externalpackage', 'logs'))
) # type: ignore ) # type: ignore
pypi_mirror_provider: bpy.props.StringProperty( pypi_mirror_provider: bpy.props.StringProperty(
@@ -40,7 +40,6 @@ class ExternalPackagePreferences():
row_import = col_main.row() row_import = col_main.row()
row_import.prop(self, 'pypi_mirror_provider', text='Set PyPI Mirror') row_import.prop(self, 'pypi_mirror_provider', text='Set PyPI Mirror')
installer = PackageInstaller( installer = PackageInstaller(
pypi_mirror_provider=self.pypi_mirror_provider, pypi_mirror_provider=self.pypi_mirror_provider,
log_dir=self.log_dir, log_dir=self.log_dir,

View File

@@ -446,18 +446,18 @@ class ExportVDB(bpy.types.Operator):
filename_ext = ".vdb" filename_ext = ".vdb"
def invoke(self, context, event): @classmethod
def poll(cls, context):
bioxels_obj = None bioxels_obj = None
for obj in bpy.context.selected_objects: for obj in bpy.context.selected_objects:
bioxels_obj = get_bioxels_obj(obj) bioxels_obj = get_bioxels_obj(obj)
break break
if bioxels_obj: return True if bioxels_obj else False
def invoke(self, context, event):
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
else:
self.report({"WARNING"}, "Cannot find any bioxels.")
return {'CANCELLED'}
def execute(self, context): def execute(self, context):
bioxels_obj = None bioxels_obj = None
@@ -466,12 +466,15 @@ class ExportVDB(bpy.types.Operator):
break break
filepath = f"{self.filepath.split('.')[0]}.vdb" filepath = f"{self.filepath.split('.')[0]}.vdb"
# "//"
source_dir = bpy.path.abspath(bioxels_obj.data.filepath)
output_path: Path = Path(filepath).resolve() output_path: Path = Path(filepath).resolve()
source_path: Path = Path(bioxels_obj.data.filepath).resolve() source_path: Path = Path(source_dir).resolve()
# print('output_path', output_path) # print('output_path', output_path)
# print('source_path', source_path) # print('source_path', source_path)
shutil.copy(source_path, output_path) shutil.copy(source_path, output_path)
self.report({"INFO"}, "Successfully Exported") self.report({"INFO"}, f"Successfully exported to {output_path}")
return {'FINISHED'} return {'FINISHED'}

View File

@@ -1,8 +1,20 @@
from bioxelnodes.misc import SaveBioxels
import bpy import bpy
from .convert import ConvertToMesh from .convert import ConvertToMesh
from .io import ExportVDB, ImportDICOM from .io import ExportVDB, ImportDICOM
class ObjectMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_OBJECT"
bl_label = "Bioxels"
def draw(self, context):
layout = self.layout
layout.operator(SaveBioxels.bl_idname)
layout.separator()
layout.operator(ConvertToMesh.bl_idname)
def TOPBAR_FILE_IMPORT(self, context): def TOPBAR_FILE_IMPORT(self, context):
layout = self.layout layout = self.layout
layout.separator() layout.separator()
@@ -17,7 +29,8 @@ def TOPBAR_FILE_EXPORT(self, context):
def VIEW3D_OBJECT(self, context): def VIEW3D_OBJECT(self, context):
layout = self.layout layout = self.layout
layout.operator(ConvertToMesh.bl_idname) layout.menu(ObjectMenu.bl_idname)
layout.separator() layout.separator()

65
bioxelnodes/misc.py Normal file
View File

@@ -0,0 +1,65 @@
from pathlib import Path
import shutil
from bioxelnodes.utils import get_bioxels_obj
import bpy
class BIOXELNODES_PT_Bioxels(bpy.types.Panel):
bl_idname = "BIOXELNODES_PT_Bioxels"
bl_label = "Bioxels"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
def draw(self, context):
layout = self.layout
scene = context.scene
layout.prop(scene, 'bioxels_dir')
class SaveBioxels(bpy.types.Operator):
bl_idname = "bioxelnodes.save_bioxels"
bl_label = "Save Bioxels"
bl_description = "Save Bioxels to Directory."
bl_options = {'UNDO'}
@classmethod
def poll(cls, context):
bioxels_obj = None
for obj in bpy.context.selected_objects:
bioxels_obj = get_bioxels_obj(obj)
break
return True if bioxels_obj else False
def execute(self, context):
bioxels_objs = []
for obj in bpy.context.selected_objects:
bioxels_obj = get_bioxels_obj(obj)
if bioxels_obj:
bioxels_objs.append(bioxels_obj)
if len(bioxels_objs) == 0:
self.report({"WARNING"}, "Cannot find any bioxels.")
return {'FINISHED'}
for bioxels_obj in bioxels_objs:
name = bioxels_obj.parent.name
# "//"
bioxels_dir = bpy.path.abspath(context.scene.bioxels_dir)
source_dir = bpy.path.abspath(bioxels_obj.data.filepath)
output_path: Path = Path(bioxels_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()
bioxels_obj.data.filepath = bpy.path.relpath(
str(output_path), start=str(blend_path))
self.report({"INFO"}, f"Successfully saved to {output_path}")
return {'FINISHED'}

View File

@@ -7,9 +7,9 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferen
bl_idname = __package__ bl_idname = __package__
cache_dir: bpy.props.StringProperty( cache_dir: bpy.props.StringProperty(
name="Python Log Directory", name="VDB Cache Directory",
subtype='DIR_PATH', subtype='DIR_PATH',
default=Path(Path.home(), '.bioxelnodes').as_posix() default=str(Path(Path.home(), '.bioxelnodes'))
) # type: ignore ) # type: ignore
def draw(self, context): def draw(self, context):
@@ -18,5 +18,5 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferen
layout.prop(self, 'cache_dir', text='Set Cache Directory') layout.prop(self, 'cache_dir', text='Set Cache Directory')
# ExternalPackagePreferences Config # ExternalPackagePreferences Config
self.requirements_dir = Path(__file__).parent.as_posix() self.requirements_dir = str(Path(__file__).parent)
super().draw(context) super().draw(context)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB