18 Commits

Author SHA1 Message Date
Ma Nan
8b45de4a4d Merge pull request #9 from OmooLab/feature/dev
fix: save all node path not right
2024-04-25 12:13:04 +08:00
2c3d9e5791 fix: save all node path not right 2024-04-25 12:12:43 +08:00
Ma Nan
32b69575b1 Merge pull request #8 from OmooLab/feature/dev
feat: save bioxels, save nodes
2024-04-24 22:48:43 +08:00
d663bfd290 feat: save bioxels, save nodes 2024-04-24 22:48:09 +08:00
Ma Nan
a1aa776725 Merge pull request #7 from OmooLab/feature/dev
Feature/dev
2024-04-24 15:07:16 +08:00
6b18b4e9fa Merge branch 'main' into feature/dev 2024-04-24 15:06:30 +08:00
a579b24f9a feat: add save bioxels function 2024-04-24 14:54:22 +08:00
Ma Nan
8b3ab61467 Update README.md 2024-04-23 15:22:51 +08:00
Ma Nan
084031c496 Merge pull request #5 from OmooLab/feature/dev
fix: get no group input node
2024-04-23 15:12:38 +08:00
15b24b61b5 fix: get no group input node 2024-04-23 15:11:44 +08:00
Ma Nan
81dae0631a Update README.md 2024-04-22 22:48:44 +08:00
Ma Nan
38d2e52c93 Merge pull request #4 from OmooLab/feature/dev
Feature/dev
2024-04-22 22:39:16 +08:00
cf6ccb1743 docs: README add badges 2024-04-22 22:35:57 +08:00
f889b61494 chore: rename packages 2024-04-22 22:35:56 +08:00
Ma Nan
436288df7c Create LICENSE 2024-04-22 17:24:30 +08:00
Ma Nan
7967f144f1 Update README.md 2024-04-22 15:06:46 +08:00
Ma Nan
ba4c6c1dee Merge pull request #3 from OmooLab/feature/dev
docs: README.md
2024-04-22 14:52:30 +08:00
ca9df9065e docs: README.md 2024-04-22 14:51:51 +08:00
27 changed files with 412 additions and 88 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 OmooLab
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

111
README.md
View File

@@ -1,3 +1,110 @@
# BioxelNodes
BioxelNodes is a blender add-on for better volume data visualization with cycles.
[中文文档](https://uj6xfhbzp0.feishu.cn/wiki/Qx3VwHuNPimeI8kr6nDcvl1DnHf?from=from_copylink)
# Bioxel Nodes
![Static Badge](https://img.shields.io/badge/Blender-orange?style=for-the-badge&logo=blender&logoColor=white)
![GitHub License](https://img.shields.io/github/license/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)
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.
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)
### 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
Currently only support Blender 4.1 or above, make sure you have the correct version of Blender.
### Add-on Installation
Download the latest version https://github.com/OmooLab/BioxelNodes/releases/latest
In Blender, `Edit - Preferences - Add-ons - Install`, select the `BioxelNodes_{version}.zip` you just downloaded.
### Dependency Installation
The add-on requires a third-party python dependency called SimpleITK, click the Install SimpleITK button below to install the dependency. After clicking, blender may get stuck, it is downloading and installing, just wait for a moment. After the Installation, **restart blender once**.
![dependency](docs/images/dependency.png)
This step may have failed due to network factors, just click "Set PyPI Mirror" to change the mirror.
### Basic Usage
First you need to have your volumetric data ready. If not, you can access open research data from [Dryad](https://datadryad.org) (Dryad publishes data exclusively under a [Creative Commons Public Domain License](https://creativecommons.org/public-domain/cc0/))
Note: Make sure one folder contains only one sequence, multiple sequences need to be manually split into different folders first.
In Blender, `File - Import - Volume Data as Bioxels`, select **one** of the .dcm files and click on "Volume Data as Bioxels" (you can also drag one of the .dcm files directly into the 3D viewport to trigger the import, but this is limited to .dcm format files)
![importing](docs/images/importing.png)
It may take a while to read data. After finishing reading, it will pop up a dialog box
![import dialog](docs/images/import_dialog.png)
Ignore the options, just click OK!
After importing, the necessary nodes are automatically added, the reconstruction and shader are created, and then turned on the cycles rendering to directly see the result.
![result](docs/images/result.png)
Click select Bioxels Object, and open the Geometry Nodes panel to see the following node graph:
![segment node](docs/images/segment_node.png)
You can change the "Threshold" to modify the reconstruction model, or change the "Color", "Density" to modify the shader effect. All the parameters are straightforward, you can understand them by changing the values.
The node, that named "Segment", is a preset node that combines the steps of 3D reconstruction process and shading process. In general, you need to add one of the segmentation node to reconstruction first, and followed by one of the shader node to set the shader. As you can see in the following figure:
![general graph](docs/images/general_graph.png)
Currently there are 3 types of nodes
- Segmentation nodes, responsible for splitting the volume into a reconstructed model (we call them "Segment").
- Shader nodes, responsible for giving shader to the Segment.
- Slicer nodes, responsible for partially cutting the Segment.
For example:
![general graph example](docs/images/general_graph_example.png)
You can add a "Bake" node between segmentation node and shader node, if the reconstruction process consumes too much computing time. But be sure, you are selecting the volume object, not the container object.
![bake node](docs/images/bake_node.png)
# Future Features
- Support more volumetric data formats (.nii, .map, .txm...)
- Generate sections
- More segmentation methods, e.g. AI segmentation.
- Even better shader for volumetric rendering
# Technical Notes
The "Bioxel" in "BioxelNodes", is a combination of the words "Bio-" and "Voxel". Bioxel is a voxel that stores biological data, so maybe its chinese name should be "生素" 😂? The volumetric data made up of Bioxel is called Bioxels. We are developing a toolkit around Bioxels for better biological data visualization. but before its release, we made this Blender version of bioxels toolkit first, in order to let more people to have fun with volumetric data.
### Bioxels
Bioxels is based on the RAS coordinate system, Right Aanterior Superior, which was chosen over LPS because it is more compatible with most 3D CG software coordinate systems, and is in line with the 3D artist's understanding of space.
All distances within Bioxels are in Units, and are specified in Meter pre unit. However, when Bioxels is imported into 3D CG software, its size in the software is not scaled by reading the Meter pre unit directly. The reason for this is that many 3D operations in software require that the primtives not be too large or too small.
### Based on OpenVDB
Bioxels is based entirely on OpenVDB for storage and rendering. The main reason for choosing OpenVDB is that as a volumetric data format, it is the fastest way to work with most CG renderers.
### Based on Geometry Nodes
Bioxel Nodes relies on Blender Geometry Nodes to reconstruct and render volumetric data. Node-based operations ensure that the original data is not permanently altered during reconstruction and rendering operations. The fact that the processing is based on Geometry Nodes without any additional dependencies also ensures that Blender can open files without this plugin installed. Look for more support for OpenVDB in GeometryNodes so that Bioxel Nodes can do more in the future.

View File

@@ -6,18 +6,24 @@ import bpy
bl_info = {
"name": "BiovelNodes",
"name": "Bioxel Nodes",
"author": "Ma Nan",
"description": "",
"blender": (4, 0, 0),
"blender": (4, 1, 0),
"version": (0, 1, 0),
"location": "File -> Import",
"warning": "",
"category": "Import-Export"
"category": "Node"
}
auto_load.init()
bpy.types.Scene.bioxels_dir = bpy.props.StringProperty(
name="Bioxels Directory",
subtype='DIR_PATH',
default="//"
)
def register():
auto_load.register()

View File

@@ -1,5 +1,5 @@
import bpy
from .utils import get_bioxels_obj
from .utils import get_bioxels_obj, get_node_by_type
class ConvertToMesh(bpy.types.Operator):
@@ -8,6 +8,15 @@ class ConvertToMesh(bpy.types.Operator):
bl_description = "Convert Bioxels To Mesh."
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 = []
@@ -16,7 +25,7 @@ class ConvertToMesh(bpy.types.Operator):
if bioxels_obj:
bioxels_objs.append(bioxels_obj)
if len(bioxels_objs)==0:
if len(bioxels_objs) == 0:
self.report({"WARNING"}, "Cannot find any bioxels.")
return {'FINISHED'}
@@ -31,7 +40,7 @@ class ConvertToMesh(bpy.types.Operator):
nodes = modifier.node_group.nodes
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")
realize_nodes = nodes.new("GeometryNodeRealizeInstances")
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.ops.object.material_slot_remove()
self.report({"INFO"}, f"Successfully convert to mesh")
return {'FINISHED'}

View File

@@ -1 +0,0 @@
from .node_menu import CustomNodes

View File

@@ -0,0 +1 @@
from .menu import CustomNodes

View File

@@ -1,7 +1,65 @@
import shutil
import bpy
from pathlib import Path
from .nodes import AddCustomNode
bpy.types.Scene.custom_nodes_dir = bpy.props.StringProperty(
name="Nodes Directory",
subtype='DIR_PATH',
default="//"
)
class SaveAllNodes(bpy.types.Operator):
bl_idname = "customnodes.save_all_nodes"
bl_label = "Save All Custom 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
# "//"
custom_nodes_dir = bpy.path.abspath(context.scene.custom_nodes_dir)
output_path: Path = Path(custom_nodes_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, 'custom_nodes_dir')
layout.operator(SaveAllNodes.bl_idname)
class CustomNodes():
def __init__(
@@ -12,10 +70,10 @@ class CustomNodes():
root_icon='NONE'
) -> None:
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.nodes_file = Path(nodes_file).resolve().as_posix()
self.nodes_file = str(Path(nodes_file).resolve())
self.root_label = root_label
self.root_icon = root_icon
@@ -28,7 +86,7 @@ class CustomNodes():
)
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):
if ('GeometryNodeTree' == bpy.context.area.spaces[0].tree_type):
@@ -40,7 +98,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 "CUSTOMNODES_MT_NODES"
idname = f"{idname_namespace}_{label.replace(' ', '').upper()}"
# create submenu class if item is menu.
@@ -60,6 +118,7 @@ class CustomNodes():
class Menu(bpy.types.Menu):
bl_idname = idname
bl_label = label
nodes_file = self.nodes_file
def draw(self, context):
layout = self.layout

View File

@@ -231,7 +231,7 @@ class PackageInstaller():
"""
# path to python.exe
python_exe = Path(sys.executable).resolve().as_posix()
python_exe = str(Path(sys.executable).resolve())
# build the command list
cmd_list = [python_exe] + cmd_list
@@ -373,7 +373,7 @@ class EXTERNALPACKAGE_OT_Install_Package(bpy.types.Operator):
)
result = installer.install_package(f"{self.package}=={self.version}")
if result.returncode == 0 and installer.is_installed(self.package):
if result.returncode == 0 or installer.is_installed(self.package):
self.report(
{'INFO'},
f"Successfully installed {self.package} v{self.version}"
@@ -381,9 +381,7 @@ class EXTERNALPACKAGE_OT_Install_Package(bpy.types.Operator):
else:
self.report(
{'ERROR'},
f"""
Error installing package. Please check the log files
in {preferences.log_dir}.
"""
f"""Error installing package. Please check the log files
in {preferences.log_dir}."""
)
return {'FINISHED'}

View File

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

View File

@@ -6,7 +6,7 @@ import numpy as np
from pathlib import Path
from uuid import uuid4
import mathutils
from .utils import calc_bbox_verts, extract_last_number, get_bioxels_obj, show_message
from .utils import calc_bbox_verts, extract_last_number, get_bioxels_obj, get_node_by_type, show_message
from .nodes import custom_nodes
SUPPORT_EXTS = ['.dcm', '.tif', '.tiff', '.png', '.bmp', '', '.jpg']
@@ -23,7 +23,7 @@ def get_data_files(filepath: str):
return extract_last_number(file.stem)
files.sort(key=last_number)
files = [f.as_posix() for f in files]
files = [str(f) for f in files]
return files
@@ -69,8 +69,8 @@ def on_info_changed(self, context):
class ImportDICOMDialog(bpy.types.Operator):
bl_idname = "bioxelnodes.import_dicom_dialog"
bl_label = "Volume Data as Biovels"
bl_description = "Import Volume Data as Biovels (VDB)."
bl_label = "Volume Data as Bioxels"
bl_description = "Import Volume Data as Bioxels (VDB)."
bl_options = {'UNDO'}
filepath: bpy.props.StringProperty(
@@ -79,7 +79,7 @@ class ImportDICOMDialog(bpy.types.Operator):
) # type: ignore
bioxels_shape: bpy.props.IntVectorProperty(
name="Bioxels Shape",
name="Bioxels Shape (ReadOnly)",
min=0,
default=(100, 100, 100)
) # type: ignore
@@ -128,23 +128,13 @@ class ImportDICOMDialog(bpy.types.Operator):
) # type: ignore
def execute(self, context):
if not self.filepath:
self.report({"WARNING"}, "Get no file path.")
return {'CANCELLED'}
if Path(self.filepath).suffix not in SUPPORT_EXTS:
self.report({"WARNING"}, "Not Supported extension.")
return {'CANCELLED'}
file_path = Path(self.filepath).resolve()
name = file_path.parent.name
files = get_data_files(self.filepath)
name = Path(self.filepath).parent.name
import SimpleITK as sitk
image = sitk.ReadImage(files)
bioxel_size = float(self.bioxel_size)
orig_spacing = tuple(self.orig_spacing)
image_spacing = image.GetSpacing()
image_shape = image.GetSize()
@@ -161,9 +151,6 @@ class ImportDICOMDialog(bpy.types.Operator):
int(image_shape[2] / bioxel_size * orig_spacing[2]),
)
print("Bioxel Size:", bioxel_size)
print("Bioxels Shape:", bioxels_shape)
print("Resampling...")
image = sitk.Resample(
image1=image,
@@ -177,14 +164,12 @@ class ImportDICOMDialog(bpy.types.Operator):
outputPixelType=image.GetPixelID(),
)
try:
image = sitk.DICOMOrient(image, 'RAS')
except:
...
print("Orienting to RAS...")
image = sitk.DICOMOrient(image, 'RAS')
array = sitk.GetArrayFromImage(image)
orig_dtype = str(array.dtype)
print(f"Coverting Dtype {orig_dtype} -> float")
print(f"Coverting Dtype from {orig_dtype} to float...")
array = array.astype(float)
# ITK indices, by convention, are [i,j,k] while NumPy indices are [k,j,i]
@@ -198,6 +183,10 @@ class ImportDICOMDialog(bpy.types.Operator):
array = np.transpose(array)
bioxels_max = float(np.max(array))
bioxels_min = float(np.min(array))
bioxels_shape = array.shape
print("Bioxel Size:", bioxel_size)
print("Bioxels Shape:", bioxels_shape)
bioxels_offset = 0.0
if bioxels_min < 0 and orig_dtype[0] != "u":
@@ -243,13 +232,13 @@ class ImportDICOMDialog(bpy.types.Operator):
vdb_dirpath.mkdir(parents=True, exist_ok=True)
vdb_path = Path(vdb_dirpath, f"{uuid4()}.vdb")
print(f"Storing the VDB file ({vdb_path.as_posix()})...")
vdb.write(vdb_path.as_posix(), grids=[grid])
print(f"Storing the VDB file ({str(vdb_path)})...")
vdb.write(str(vdb_path), grids=[grid])
# Read VDB
print(f"Importing the VDB file to Blender scene...")
bpy.ops.object.volume_import(
filepath=vdb_path.as_posix(), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
filepath=str(vdb_path), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
bioxels_obj = bpy.context.active_object
@@ -274,7 +263,6 @@ class ImportDICOMDialog(bpy.types.Operator):
)
bioxels_obj.matrix_world = mat_ras2blender @ mat_scene_scale
bioxels_obj.lock_location[0] = True
bioxels_obj.lock_location[1] = True
bioxels_obj.lock_location[2] = True
@@ -297,10 +285,8 @@ class ImportDICOMDialog(bpy.types.Operator):
container_obj = bpy.context.active_object
bbox_verts = calc_bbox_verts((0, 0, 0), bioxels_shape)
for index, vert in enumerate(container_obj.data.vertices):
co = transfrom @ mathutils.Vector(bbox_verts[index])
co = mat_ras2blender @ co
co = mat_scene_scale @ co
vert.co = co
bbox_transform = mat_ras2blender @ mat_scene_scale @ transfrom
vert.co = bbox_transform @ mathutils.Vector(bbox_verts[index])
bioxels_obj.parent = container_obj
container_obj.name = name
@@ -333,10 +319,11 @@ class ImportDICOMDialog(bpy.types.Operator):
nodes = node_tree.nodes
links = node_tree.links
input_node = nodes.get("Group Input")
output_node = nodes.get("Group Output")
segment_node = custom_nodes.add_node(nodes, 'BioxelNodes_Segment')
input_node = get_node_by_type(nodes, 'NodeGroupInput')[0]
output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0]
segment_node = custom_nodes.add_node(
nodes, 'BioxelNodes_Segment')
links.new(input_node.outputs[0], segment_node.inputs[0])
links.new(segment_node.outputs[0], output_node.inputs[0])
@@ -360,8 +347,8 @@ class ImportDICOMDialog(bpy.types.Operator):
class ReadDICOM(bpy.types.Operator):
bl_idname = "bioxelnodes.read_dicom"
bl_label = "Volume Data as Biovels"
bl_description = "Import Volume Data as Biovels (VDB)."
bl_label = "Volume Data as Bioxels"
bl_description = "Import Volume Data as Bioxels (VDB)."
bl_options = {'UNDO'}
filepath: bpy.props.StringProperty(
@@ -373,9 +360,14 @@ class ReadDICOM(bpy.types.Operator):
) # type: ignore
def execute(self, context):
if Path(self.filepath).suffix not in SUPPORT_EXTS:
self.report({"WARNING"}, "Not Supported extension.")
return {'CANCELLED'}
files = get_data_files(self.filepath)
import SimpleITK as sitk
image = sitk.ReadImage(files)
print("Collecting Meta Data...")
@@ -403,9 +395,6 @@ class ReadDICOM(bpy.types.Operator):
if not self.filepath:
return {'CANCELLED'}
if Path(self.filepath).suffix not in SUPPORT_EXTS:
return {'CANCELLED'}
show_message('Reading image data, it may take a while',
'Please be patient...')
context.window_manager.modal_handler_add(self)
@@ -414,8 +403,8 @@ class ReadDICOM(bpy.types.Operator):
class ImportDICOM(bpy.types.Operator):
bl_idname = "bioxelnodes.import_dicom"
bl_label = "Volume Data as Biovels"
bl_description = "Import Volume Data as Biovels (VDB)."
bl_label = "Volume Data as Bioxels"
bl_description = "Import Volume Data as Bioxels (VDB)."
bl_options = {'UNDO'}
filepath: bpy.props.StringProperty(
@@ -457,18 +446,18 @@ class ExportVDB(bpy.types.Operator):
filename_ext = ".vdb"
def invoke(self, context, event):
@classmethod
def poll(cls, context):
bioxels_obj = None
for obj in bpy.context.selected_objects:
bioxels_obj = get_bioxels_obj(obj)
break
if bioxels_obj:
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
else:
self.report({"WARNING"}, "Cannot find any bioxels.")
return {'CANCELLED'}
return True if bioxels_obj else False
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
bioxels_obj = None
@@ -477,10 +466,15 @@ class ExportVDB(bpy.types.Operator):
break
filepath = f"{self.filepath.split('.')[0]}.vdb"
# "//"
source_dir = bpy.path.abspath(bioxels_obj.data.filepath)
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('source_path', source_path)
shutil.copy(source_path, output_path)
self.report({"INFO"}, f"Successfully exported to {output_path}")
return {'FINISHED'}

View File

@@ -1,8 +1,20 @@
from bioxelnodes.misc import SaveBioxels
import bpy
from .convert import ConvertToMesh
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):
layout = self.layout
layout.separator()
@@ -17,7 +29,8 @@ def TOPBAR_FILE_EXPORT(self, context):
def VIEW3D_OBJECT(self, context):
layout = self.layout
layout.operator(ConvertToMesh.bl_idname)
layout.menu(ObjectMenu.bl_idname)
layout.separator()

103
bioxelnodes/misc.py Normal file
View File

@@ -0,0 +1,103 @@
from pathlib import Path
import shutil
from bioxelnodes.utils import get_all_bioxels_objs, get_bioxels_obj
import bpy
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 bioxels")
return {'FINISHED'}
class SaveAllBioxels(bpy.types.Operator):
bl_idname = "bioxelnodes.save_all_bioxels"
bl_label = "Save All Bioxels"
bl_description = "Save All Bioxels to Directory."
bl_options = {'UNDO'}
def execute(self, context):
bioxels_objs = get_all_bioxels_objs()
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 bioxels")
return {'FINISHED'}
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')
layout.operator(SaveAllBioxels.bl_idname)

View File

@@ -1,6 +1,6 @@
from pathlib import Path
from .custom_nodes import CustomNodes
from .customnodes import CustomNodes
import bpy
@@ -9,8 +9,8 @@ def add_driver_to_node_factory(source_prop, target_prop):
import bpy
from bioxelnodes.utils import add_direct_driver, get_bioxels_obj
bioxels_obj = get_bioxels_obj(bpy.context.active_object)
container_obj = bioxels_obj.parent
if bioxels_obj:
container_obj = bioxels_obj.parent
add_direct_driver(
target=node,
target_prop='{target_prop}',
@@ -28,9 +28,10 @@ def set_prop_to_node_factory(source_prop, target_prop):
import bpy
from bioxelnodes.utils import get_bioxels_obj
bioxels_obj = get_bioxels_obj(bpy.context.active_object)
container_obj = bioxels_obj.parent
if bioxels_obj:
node.inputs.get('{target_prop}').default_value = container_obj.get('{source_prop}')
container_obj = bioxels_obj.parent
node.inputs.get('{target_prop}').default_value = container_obj.get(
'{source_prop}')
else:
print('Cannot find any bioxels.')
"""
@@ -44,7 +45,7 @@ MENU_ITEMS = [
'items': [
{
'label': 'Segment',
'icon': 'OUTLINER_OB_META',
'icon': 'BONE_DATA',
'node_type': 'BioxelNodes_Segment',
'node_description': '',
'node_callback': "\n".join([
@@ -54,7 +55,7 @@ MENU_ITEMS = [
},
{
'label': 'Anatomy',
'icon': 'OUTLINER_OB_META',
'icon': 'CONSTRAINT_BONE',
'node_type': 'BioxelNodes_Anatomy',
'node_description': '',
'node_callback': "\n".join([
@@ -140,7 +141,7 @@ MENU_ITEMS = [
custom_nodes = CustomNodes(
menu_items=MENU_ITEMS,
nodes_file=Path(__file__).parent/"assets/Nodes.blend",
root_label='BioxelNodes',
nodes_file=Path(__file__).parent/"assets/Nodes/BioxelNodes.blend",
root_label='Bioxel Nodes',
root_icon="FILE_VOLUME",
)

View File

@@ -1,23 +1,22 @@
import bpy
from pathlib import Path
from .external_package import ExternalPackagePreferences
from .externalpackage import ExternalPackagePreferences
class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferences):
bl_idname = __package__
cache_dir: bpy.props.StringProperty(
name="Python Log Directory",
name="VDB Cache Directory",
subtype='DIR_PATH',
default=Path(Path.home(), '.bioxelnodes').as_posix()
default=str(Path(Path.home(), '.bioxelnodes'))
) # type: ignore
def draw(self, context):
layout = self.layout
layout.label(text="Configuration")
layout.prop(self, 'cache_dir', text='Set Cache Directory')
# ExternalPackagePreferences Config
self.requirements_dir = Path(__file__).parent.as_posix()
self.requirements_dir = str(Path(__file__).parent)
super().draw(context)

View File

@@ -2,6 +2,10 @@ import bpy
import mathutils
def get_node_by_type(nodes, type_name: str):
return [node for node in nodes if type(node).__name__ == type_name]
def show_message(message="", title="Message Box", icon='INFO'):
def draw(self, context):
@@ -77,6 +81,15 @@ def get_bioxels_obj(current_obj):
return bioxels_obj
def get_all_bioxels_objs():
bioxels_objs = []
for obj in bpy.data.objects:
if obj.parent:
if obj.parent.get('bioxels_container') and obj.get('bioxels'):
bioxels_objs.append(obj)
return list(set(bioxels_objs))
def extract_last_number(text):
# Initialize an empty string to store the digits

BIN
docs/images/bake_node.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
docs/images/dependency.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
docs/images/gallery.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
docs/images/importing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

BIN
docs/images/result.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB