feat: add save bioxels function
This commit is contained in:
14
README.md
14
README.md
@@ -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:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### 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
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
# 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...)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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
65
bioxelnodes/misc.py
Normal 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'}
|
||||||
@@ -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 |
Reference in New Issue
Block a user