fix: eevee specular too high

feat: add pie cutter
feat: support 4d time data
chora: change operators name
docs: update README.md
This commit is contained in:
2024-07-15 14:25:51 +08:00
parent 8b862b3fe0
commit b2791e3aae
42 changed files with 928 additions and 497 deletions

View File

@@ -9,7 +9,7 @@ bl_info = {
"author": "Ma Nan",
"description": "",
"blender": (4, 1, 0),
"version": (0, 2, 6),
"version": (0, 2, 7),
"location": "File -> Import",
"warning": "",
"category": "Node"

View File

@@ -13,7 +13,7 @@ from . import skimage as ski
from .nodes import custom_nodes
from .props import BIOXELNODES_Series
from .utils import (calc_bbox_verts, get_all_layers, get_container_from_selection, get_layer, get_text_index_str,
get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, show_message)
get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, save_vdbs, select_object, show_message)
try:
import SimpleITK as sitk
@@ -124,7 +124,11 @@ def parse_volume_data(filepath: str, series_id=""):
series_files = reader.GetGDCMSeriesFileNames(
str(dir_path), series_id)
reader.SetFileNames(series_files)
itk_volume = reader.Execute()
# for k in reader.GetMetaDataKeys(0):
# v = reader.GetMetaData(0, k)
# print(f'({k}) = = "{v}"')
name = dir_path.name
elif ext in SEQUENCE_EXTS:
@@ -140,32 +144,64 @@ def parse_volume_data(filepath: str, series_id=""):
itk_volume = sitk.ReadImage(filepath)
name = Path(filepath).name.removesuffix(ext)
itk_volume = sitk.DICOMOrient(itk_volume, 'RAS')
volume = sitk.GetArrayFromImage(itk_volume)
# for key in itk_volume.GetMetaDataKeys():
# print(f"{key},{itk_volume.GetMetaData(key)}")
if itk_volume.GetDimension() == 3:
itk_volume = sitk.DICOMOrient(itk_volume, 'RAS')
meta = {
"name": name,
"shape": tuple(itk_volume.GetSize()),
"spacing": tuple(itk_volume.GetSpacing()),
"origin": tuple(itk_volume.GetOrigin()),
"direction": tuple(itk_volume.GetDirection()),
"is_oriented": True
}
volume = sitk.GetArrayFromImage(itk_volume)
# transpose ijk to kji
if volume.ndim == 4:
volume = np.transpose(volume, (2, 1, 0, 3))
else:
volume = np.transpose(volume)
volume = np.expand_dims(volume, axis=-1)
if volume.ndim == 4:
volume = np.transpose(volume, (2, 1, 0, 3))
else:
volume = np.transpose(volume)
# FIXME: not sure...
print(itk_volume.GetDirection())
direction = np.array(itk_volume.GetDirection())
direction = direction.reshape(3, 3) if itk_volume.GetDimension() == 3 \
else direction.reshape(4, 4)
meta = {
"name": name,
"shape": tuple(itk_volume.GetSize()),
"spacing": tuple(itk_volume.GetSpacing()),
"origin": tuple(itk_volume.GetOrigin()),
"direction": tuple(itk_volume.GetDirection()),
}
direction = direction[1:, 1:]
direction = tuple(direction.flatten())
for key, value in meta.items():
print(f"{key}: {value}")
meta = {
"name": name,
"shape": tuple(itk_volume.GetSize()[:3]),
"spacing": tuple(itk_volume.GetSpacing()[:3]),
"origin": tuple(itk_volume.GetOrigin()[:3]),
"direction": direction,
"is_oriented": False
}
volume = sitk.GetArrayFromImage(itk_volume)
if volume.ndim == 5:
volume = np.transpose(volume, (0, 3, 2, 1, 4))
else:
volume = np.transpose(volume, (0, 3, 2, 1))
volume = np.expand_dims(volume, axis=-1)
return volume, meta
class ImportVolumeDataDialog(bpy.types.Operator):
bl_idname = "bioxelnodes.import_volume_data_dialog"
bl_label = "Volume Data as Bioxel Layer"
bl_description = "Import Volume Data as Bioxel Layer"
bl_label = "Volumetric Data as Bioxel Layer"
bl_description = "Import Volumetric Data as Bioxel Layer"
bl_options = {'UNDO'}
filepath: bpy.props.StringProperty(
@@ -188,7 +224,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
name="Read as",
default="scalar",
items=[("scalar", "Scalar", ""),
("labels", "Labels", "")]
("label", "Labels", "")]
) # type: ignore
bioxel_size: bpy.props.FloatProperty(
@@ -215,9 +251,14 @@ class ImportVolumeDataDialog(bpy.types.Operator):
default=0.01,
) # type: ignore
do_orient: bpy.props.BoolProperty(
name="Orient to RAS",
default=True,
is_time_sequence: bpy.props.BoolProperty(
name="Is Time Sequence",
default=False,
) # type: ignore
split_channels: bpy.props.BoolProperty(
name="Split Channels",
default=False,
) # type: ignore
def execute(self, context):
@@ -259,10 +300,8 @@ class ImportVolumeDataDialog(bpy.types.Operator):
bioxel_size, 4
)
# transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale \
# if self.do_orient else mat_location @ mat_rotation @ mat_scale
transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale
transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale \
if meta['is_oriented'] else mat_location @ mat_rotation @ mat_scale
# Wrapper a Container
if not self.container:
@@ -308,26 +347,62 @@ class ImportVolumeDataDialog(bpy.types.Operator):
container_node_group = container.modifiers[0].node_group
preferences = context.preferences.addons[__package__].preferences
loc, rot, sca = transfrom.decompose()
# TODO: change to transform when 4.2?
loc, rot, sca = transfrom.decompose()
layer_origin = tuple(loc)
layer_rotation = tuple(rot.to_euler())
def create_layer(volume, layer_name, layer_type="scalar"):
grid = vdb.FloatGrid()
volume = volume.copy().astype(np.float32)
grid.copyFromArray(volume)
# grid.transform = vdb.createLinearTransform(transfrom.transposed())
grid.name = layer_type
def create_layer(volume, layer_name, layer_shape, layer_type="scalar"):
if volume.ndim == 4:
grids_sequence = []
for f in range(volume.shape[0]):
print(f"Resampling...")
frame = ski.resize(volume[f, :, :, :],
layer_shape,
preserve_range=True,
anti_aliasing=volume.dtype.kind != "b")
vdb_path = save_vdb([grid], context)
grid = vdb.FloatGrid()
frame = frame.copy().astype(np.float32)
grid.copyFromArray(frame)
grid.transform = vdb.createLinearTransform(
transfrom.transposed())
grid.name = layer_type
grids_sequence.append([grid])
# Read VDB
print(f"Loading the cache to Blender scene...")
bpy.ops.object.volume_import(
filepath=str(vdb_path), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
vdb_paths = save_vdbs(grids_sequence, context)
# Read VDB
print(f"Loading the cache to Blender scene...")
files = [{"name": str(vdb_path.name), "name": str(vdb_path.name)}
for vdb_path in vdb_paths]
bpy.ops.object.volume_import(filepath=str(vdb_paths[0]), directory=str(vdb_paths[0].parent),
files=files, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
else:
volume = ski.resize(volume,
layer_shape,
preserve_range=True,
anti_aliasing=volume.dtype.kind != "b")
grid = vdb.FloatGrid()
volume = volume.copy().astype(np.float32)
grid.copyFromArray(volume)
grid.transform = vdb.createLinearTransform(
transfrom.transposed())
grid.name = layer_type
vdb_path = save_vdb([grid], context)
# Read VDB
print(f"Loading the cache to Blender scene...")
bpy.ops.object.volume_import(filepath=str(vdb_path),
align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
layer = bpy.context.active_object
layer.data.sequence_mode = 'REPEAT'
# Set props to VDB object
layer.name = layer_name
@@ -368,7 +443,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
output_node.inputs[0])
# for compatibility to old vdb
to_layer_node.inputs['Not Transfromed'].default_value = True
# to_layer_node.inputs['Not Transfromed'].default_value = True
to_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
200000000)
to_layer_node.inputs['Data Type'].default_value = dtype_index
@@ -381,96 +456,118 @@ class ImportVolumeDataDialog(bpy.types.Operator):
return layer
if self.read_as == "labels":
if volume.ndim == 4:
volume = np.amax(volume, -1)
volume = volume.astype(int)
orig_max = int(np.max(volume))
orig_min = int(np.min(volume))
layer_name = self.layer_name or "Label"
for i in range(orig_max):
label = volume == np.full_like(volume, i+1)
print(f"Resampling...")
label = ski.resize(label,
layer_shape,
preserve_range=True,
anti_aliasing=False)
layer = create_layer(volume=label,
layer_name=f"{container_name}_{layer_name}_{i+1}",
layer_type="label")
mask_node = custom_nodes.add_node(container_node_group,
'BioxelNodes_MaskByLabel')
mask_node.label = f"{layer_name}_{i+1}"
mask_node.inputs[0].default_value = layer
# Connect to output if no output linked
output_node = get_nodes_by_type(container_node_group,
'NodeGroupOutput')[0]
if len(output_node.inputs[0].links) == 0:
container_node_group.links.new(mask_node.outputs[0],
output_node.inputs[0])
move_node_to_node(mask_node, output_node, (-300, 0))
else:
move_node_to_node(mask_node, output_node,
(0, -100 * (i+1)))
else:
if volume.ndim == 4:
volume = np.amax(volume, -1)
# volume = skimage.color.rgb2gray(volume)
# if volume.dtype.kind == 'u':
# imax_in = np.iinfo(volume.dtype).max
# volume = np.multiply(volume, 255.0 / imax_in, dtype=np.float32)
# elif volume.dtype.kind == 'i':
# volume = volume.astype(np.float32)
# should not change any value!
volume = volume.astype(np.float32)
print(f"Resampling...")
volume = ski.resize(volume,
layer_shape,
anti_aliasing=True)
orig_max = float(np.max(volume))
orig_min = float(np.min(volume))
scalar_offset = 0
if orig_min < 0:
scalar_offset = -orig_min
volume = volume + np.full_like(volume, scalar_offset)
layer_name = self.layer_name or "Scalar"
layer = create_layer(volume=volume,
layer_name=f"{container_name}_{layer_name}",
layer_type="scalar")
layer_node_group = layer.modifiers[0].node_group
to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer']
to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
to_layer_node.inputs['Scalar Max'].default_value = orig_max
to_layer_node.inputs['Scalar Min'].default_value = orig_min
def create_mask_node(layer, node_type, node_label, offset):
mask_node = custom_nodes.add_node(container_node_group,
'BioxelNodes_MaskByThreshold')
mask_node.label = layer_name
node_type)
mask_node.label = node_label
mask_node.inputs[0].default_value = layer
# Connect to output if no output linked
output_node = get_nodes_by_type(container_node_group,
'NodeGroupOutput')[0]
if len(output_node.inputs[0].links) == 0:
container_node_group.links.new(mask_node.outputs[0],
output_node.inputs[0])
move_node_to_node(mask_node, output_node, (-300, 0))
else:
move_node_to_node(mask_node, output_node, (0, -100))
move_node_to_node(mask_node, output_node, offset)
bpy.context.view_layer.objects.active = container
# change shape as sequence or not
if self.is_time_sequence:
# 4->5 or 5->5
if volume.ndim == 4:
# channel as frame
volume = volume.transpose(3, 0, 1, 2)
volume = np.expand_dims(volume, axis=-1)
else:
# 4->4 or 5->4
if volume.ndim == 5:
# select frame 0
volume = volume[0, :, :, :, :]
if self.read_as == "label":
layer_name = self.layer_name or "Label"
volume = np.amax(volume, -1)
volume = volume.astype(int)
orig_max = int(np.max(volume))
orig_min = int(np.min(volume))
for i in range(orig_max):
label = volume == np.full_like(volume, i+1)
layer_name_i = f"{layer_name}_{i+1}"
layer = create_layer(volume=label,
layer_name=f"{container_name}_{layer_name_i}",
layer_shape=layer_shape,
layer_type="label")
mask_node = create_mask_node(layer=layer,
node_type='BioxelNodes_MaskByLabel',
node_label=layer_name_i,
offset=(0, -100 * (i+1)))
else:
layer_name = self.layer_name or "Scalar"
# SHOULD NOT change any value!
volume = volume.astype(np.float32)
if self.split_channels:
for i in range(volume.shape[-1]):
scalar = volume[:, :, :, :,
0] if self.is_time_sequence else volume[:, :, :, i]
orig_max = float(np.max(scalar))
orig_min = float(np.min(scalar))
scalar_offset = 0
if orig_min < 0:
scalar_offset = -orig_min
scalar = scalar + np.full_like(scalar, scalar_offset)
layer_name_i = f"{layer_name}_{i+1}"
layer = create_layer(volume=scalar,
layer_name=f"{container_name}_{layer_name_i}",
layer_shape=layer_shape,
layer_type="scalar")
layer_node_group = layer.modifiers[0].node_group
to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer']
to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
to_layer_node.inputs['Scalar Max'].default_value = orig_max
to_layer_node.inputs['Scalar Min'].default_value = orig_min
mask_node = create_mask_node(layer=layer,
node_type='BioxelNodes_MaskByThreshold',
node_label=layer_name_i,
offset=(0, -100 * (i+1)))
else:
volume = np.amax(volume, -1)
orig_max = float(np.max(volume))
orig_min = float(np.min(volume))
scalar_offset = 0
if orig_min < 0:
scalar_offset = -orig_min
volume = volume + np.full_like(volume, scalar_offset)
layer = create_layer(volume=volume,
layer_name=f"{container_name}_{layer_name}",
layer_shape=layer_shape,
layer_type="scalar")
layer_node_group = layer.modifiers[0].node_group
to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer']
to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
to_layer_node.inputs['Scalar Max'].default_value = orig_max
to_layer_node.inputs['Scalar Min'].default_value = orig_min
mask_node = create_mask_node(layer=layer,
node_type='BioxelNodes_MaskByThreshold',
node_label=layer_name,
offset=(0, -100))
select_object(container)
# Change render setting for better result
if preferences.do_change_render_setting and is_first_import:
@@ -492,7 +589,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
return {'FINISHED'}
def invoke(self, context, event):
if self.read_as == "labels":
if self.read_as == "label":
volume_dtype = "Label"
elif self.read_as == "scalar":
volume_dtype = "Scalar"
@@ -529,6 +626,11 @@ class ImportVolumeDataDialog(bpy.types.Operator):
row.prop(self, "orig_spacing")
panel.label(text=layer_shape_text)
panel = layout.box()
panel.prop(self, "is_time_sequence")
if self.read_as == "scalar":
panel.prop(self, "split_channels")
if self.container == "":
panel = layout.box()
panel.prop(self, "scene_scale")
@@ -550,8 +652,8 @@ def get_series_ids(self, context):
class ParseVolumeData(bpy.types.Operator):
bl_idname = "bioxelnodes.parse_volume_data"
bl_label = "Volume Data as Bioxel Layer"
bl_description = "Import Volume Data as Bioxel Layer"
bl_label = "Volumetric Data as Bioxel Layer"
bl_description = "Import Volumetric Data as Bioxel Layer"
bl_options = {'UNDO'}
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
@@ -562,7 +664,7 @@ class ParseVolumeData(bpy.types.Operator):
name="Read as",
default="scalar",
items=[("scalar", "Scalar", ""),
("labels", "Labels", "")]
("label", "Labels", "")]
) # type: ignore
series_id: bpy.props.EnumProperty(
@@ -581,13 +683,17 @@ class ParseVolumeData(bpy.types.Operator):
print("Collecting Meta Data...")
volume, meta = parse_volume_data(self.filepath)
if self.read_as == "labels":
for key, value in meta.items():
print(f"{key}: {value}")
if self.read_as == "label":
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.")
{"WARNING"}, "This volume data does not looks like label, please check again.")
return {'CANCELLED'}
# do_orient = ext not in SEQUENCE_EXTS or ext in DICOM_EXTS
@@ -704,15 +810,15 @@ class ImportVolumeData():
class ImportAsScalarLayer(bpy.types.Operator, ImportVolumeData):
bl_idname = "bioxelnodes.import_as_scalar_layer"
bl_label = "Import as Scalar"
bl_description = "Import Volume Data to Container as Scalar"
bl_description = "Import Volumetric Data to Container as Scalar"
read_as = "scalar"
class ImportAsLabelLayer(bpy.types.Operator, ImportVolumeData):
bl_idname = "bioxelnodes.import_as_label_layer"
bl_label = "Import as Labels"
bl_description = "Import Volume Data to Container as Label"
read_as = "labels"
bl_label = "Import as Label"
bl_description = "Import Volumetric Data to Container as Label"
read_as = "label"
try:
@@ -729,40 +835,10 @@ except:
...
class AddVolumeData(bpy.types.Operator):
bl_idname = "bioxelnodes.add_volume_data"
bl_label = "Import as Bioxel Layer"
bl_description = "Import additional Volume Data to Container"
bl_options = {'UNDO'}
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore
@classmethod
def poll(cls, context):
containers = get_container_from_selection()
return len(containers) > 0
def execute(self, context):
containers = get_container_from_selection()
bpy.ops.bioxelnodes.parse_volume_data(
'INVOKE_DEFAULT',
filepath=self.filepath,
directory=self.directory,
container=containers[0].name
)
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
class ExportVolumeData(bpy.types.Operator):
bl_idname = "bioxelnodes.export_volume_data"
bl_label = "Export Bioxel as VDB"
bl_description = "Export Bioxel Layer as VDB"
bl_label = "Export Layer as VDB"
bl_description = "Export Layer as VDB"
bl_options = {'UNDO'}
filepath: bpy.props.StringProperty(

View File

@@ -1,12 +1,24 @@
import bpy
from .operators import (AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels,
ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange)
from .io import ExportVolumeData, ImportAsLabelLayer, ImportAsScalarLayer, ImportVolumeData, AddVolumeData
from .save import ReLinkNodes, SaveLayers, SaveAllToShare
from .utils import get_container_from_selection
from .operators import (AddPieCutter, AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels,
ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange, PickBboxWire, PickMesh, PickVolume)
from .io import ExportVolumeData, ImportAsLabelLayer, ImportAsScalarLayer
from .save import CleanAllCaches, ReLinkNodes, SaveLayers, SaveStagedData
class ModifyLayer(bpy.types.Menu):
class PickFromContainerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_PICK"
bl_label = "Pick from Container"
def draw(self, context):
layout = self.layout
layout.operator(PickMesh.bl_idname)
layout.operator(PickVolume.bl_idname)
layout.operator(PickBboxWire.bl_idname)
class ModifyLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_MODIFY_LAYERS"
bl_label = "Modify Layer"
@@ -25,20 +37,21 @@ class AddCutterMenu(bpy.types.Menu):
def draw(self, context):
layout = self.layout
layout.operator(AddPlaneCutter.bl_idname)
layout.operator(AddCylinderCutter.bl_idname)
layout.operator(AddCubeCutter.bl_idname)
layout.operator(AddSphereCutter.bl_idname)
layout.operator(AddPlaneCutter.bl_idname, text="Plane Cutter")
layout.operator(AddCylinderCutter.bl_idname, text="Cylinder Cutter")
layout.operator(AddCubeCutter.bl_idname, text="Cube Cutter")
layout.operator(AddSphereCutter.bl_idname, text="Sphere Cutter")
layout.operator(AddPieCutter.bl_idname, text="Pie Cutter")
class ImportLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_LAYERS"
bl_label = "Import Volume Data as Bioxel"
bl_label = "Import Volumetric Data"
def draw(self, context):
layout = self.layout
layout.operator(ImportAsScalarLayer.bl_idname)
layout.operator(ImportAsLabelLayer.bl_idname)
layout.operator(ImportAsScalarLayer.bl_idname, text="as Scalar")
layout.operator(ImportAsLabelLayer.bl_idname, text="as Label")
class BioxelNodesView3DMenu(bpy.types.Menu):
@@ -47,9 +60,24 @@ class BioxelNodesView3DMenu(bpy.types.Menu):
def draw(self, context):
layout = self.layout
layout.menu(ImportLayerMenu.bl_idname)
layout.menu(AddCutterMenu.bl_idname)
layout.operator(ConvertToMesh.bl_idname)
containers = get_container_from_selection()
is_selected = len(containers) > 0
layout.operator(ImportAsScalarLayer.bl_idname,
text=ImportAsScalarLayer.bl_label+" (Add to)"
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
layout.operator(ImportAsLabelLayer.bl_idname,
text=ImportAsLabelLayer.bl_label+" (Add to)"
if is_selected else ImportAsLabelLayer.bl_label+" (Init)")
layout.separator()
layout.operator(AddPlaneCutter.bl_idname)
layout.operator(AddCylinderCutter.bl_idname)
layout.operator(AddCubeCutter.bl_idname)
layout.operator(AddSphereCutter.bl_idname)
layout.operator(AddPieCutter.bl_idname)
layout.separator()
layout.operator(PickMesh.bl_idname)
layout.operator(PickVolume.bl_idname)
layout.operator(PickBboxWire.bl_idname)
layout.separator()
layout.operator(SaveLayers.bl_idname)
@@ -60,25 +88,51 @@ class BioxelNodesOutlinerMenu(bpy.types.Menu):
def draw(self, context):
layout = self.layout
layout.menu(ImportLayerMenu.bl_idname)
layout.menu(AddCutterMenu.bl_idname)
layout.operator(ConvertToMesh.bl_idname)
containers = get_container_from_selection()
is_selected = len(containers) > 0
layout.operator(ImportAsScalarLayer.bl_idname,
text=ImportAsScalarLayer.bl_label+" (Add to)"
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
layout.operator(ImportAsLabelLayer.bl_idname,
text=ImportAsLabelLayer.bl_label+" (Add to)"
if is_selected else ImportAsLabelLayer.bl_label+" (Init)")
layout.separator()
layout.menu(ModifyLayer.bl_idname)
layout.operator(AddPlaneCutter.bl_idname)
layout.operator(AddCylinderCutter.bl_idname)
layout.operator(AddCubeCutter.bl_idname)
layout.operator(AddSphereCutter.bl_idname)
layout.operator(AddPieCutter.bl_idname)
layout.separator()
layout.operator(PickMesh.bl_idname)
layout.operator(PickVolume.bl_idname)
layout.operator(PickBboxWire.bl_idname)
layout.separator()
layout.operator(SaveLayers.bl_idname)
layout.separator()
layout.operator(InvertScalar.bl_idname)
layout.operator(FillByThreshold.bl_idname)
layout.operator(FillByRange.bl_idname)
layout.operator(FillByLabel.bl_idname)
layout.operator(CombineLabels.bl_idname)
layout.separator()
layout.operator(ExportVolumeData.bl_idname)
def TOPBAR_FILE_IMPORT(self, context):
layout = self.layout
containers = get_container_from_selection()
is_selected = len(containers) > 0
layout.separator()
layout.menu(ImportLayerMenu.bl_idname)
layout.menu(ImportLayerMenu.bl_idname, text="Volumetric Data as Bioxel (Add to)"
if is_selected else "Volumetric Data as Bioxel (Init)")
def TOPBAR_FILE_EXPORT(self, context):
layout = self.layout
layout.separator()
layout.operator(ExportVolumeData.bl_idname)
layout.operator(ExportVolumeData.bl_idname,
text="Bioxel Layer (.vdb)")
def VIEW3D_OBJECT(self, context):
@@ -99,14 +153,19 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
def draw(self, context):
layout = self.layout
layout.menu(ImportLayerMenu.bl_idname)
layout.operator(ExportVolumeData.bl_idname)
containers = get_container_from_selection()
is_selected = len(containers) > 0
layout.menu(ImportLayerMenu.bl_idname, text=ImportLayerMenu.bl_label+" (Add to)"
if is_selected else ImportLayerMenu.bl_label+" (Init)")
layout.separator()
layout.menu(AddCutterMenu.bl_idname)
layout.operator(ConvertToMesh.bl_idname)
layout.menu(PickFromContainerMenu.bl_idname)
layout.separator()
layout.operator(SaveAllToShare.bl_idname)
layout.operator(SaveStagedData.bl_idname)
layout.operator(ReLinkNodes.bl_idname)
layout.separator()
layout.operator(CleanAllCaches.bl_idname)
def TOPBAR(self, context):

View File

@@ -203,6 +203,12 @@ MENU_ITEMS = [
'icon': 'MESH_UVSPHERE',
'node_type': 'BioxelNodes_SphereObjectCutter',
'node_description': '',
},
{
'label': 'Pie Cutter',
'icon': 'MESH_CONE',
'node_type': 'BioxelNodes_PieObjectCutter',
'node_description': '',
}
]
},

View File

@@ -3,12 +3,12 @@ import random
import bpy
import pyopenvdb as vdb
import numpy as np
import bmesh
from . import skimage as ski
from . import scipy
from .nodes import custom_nodes
from .utils import (get_container, get_container_from_selection, get_container_layers,
get_layer, get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb)
get_layer, get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, select_object)
def get_layer_name(layer):
@@ -105,6 +105,7 @@ def deep_copy_layer(vdb_path, base_layer, name):
# add convert to layer node
base_layer_node = base_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
not_transformed = base_layer_node.inputs['Not Transfromed'].default_value
dtype_index = base_layer_node.inputs['Data Type'].default_value
bioxel_size = base_layer_node.inputs['Bioxel Size'].default_value
layer_shape = base_layer_node.inputs['Shape'].default_value
@@ -127,7 +128,7 @@ def deep_copy_layer(vdb_path, base_layer, name):
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['Not Transfromed'].default_value = not_transformed
copyed_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
200000000)
copyed_layer_node.inputs['Data Type'].default_value = dtype_index
@@ -291,14 +292,14 @@ class InvertScalar(bpy.types.Operator):
"BioxelNodes_MaskByThreshold",
inverted_layer_name)
bpy.context.view_layer.objects.active = container
select_object(container)
return {'FINISHED'}
class FillByThreshold(bpy.types.Operator):
bl_idname = "bioxelnodes.fill_by_threshold"
bl_label = "Fill by Threshold"
bl_label = "Fill Value by Threshold"
bl_description = "Fill Value by Threshold"
bl_options = {'UNDO'}
@@ -358,7 +359,7 @@ class FillByThreshold(bpy.types.Operator):
mask_node.inputs[1].default_value = self.threshold
bpy.context.view_layer.objects.active = container
select_object(container)
return {'FINISHED'}
@@ -372,7 +373,7 @@ class FillByThreshold(bpy.types.Operator):
class FillByRange(bpy.types.Operator):
bl_idname = "bioxelnodes.fill_by_range"
bl_label = "Fill by Range"
bl_label = "Fill Value by Range"
bl_description = "Fill Value by Range"
bl_options = {'UNDO'}
@@ -441,7 +442,7 @@ class FillByRange(bpy.types.Operator):
mask_node.inputs[1].default_value = self.from_min
bpy.context.view_layer.objects.active = container
select_object(container)
return {'FINISHED'}
@@ -455,7 +456,7 @@ class FillByRange(bpy.types.Operator):
class FillByLabel(bpy.types.Operator):
bl_idname = "bioxelnodes.fill_by_label"
bl_label = "Fill by Label"
bl_label = "Fill Value by Label"
bl_description = "Fill Value by Label Area"
bl_options = {'UNDO'}
@@ -525,7 +526,7 @@ class FillByLabel(bpy.types.Operator):
"BioxelNodes_MaskByThreshold",
filled_layer_name)
bpy.context.view_layer.objects.active = container
select_object(container)
return {'FINISHED'}
@@ -585,14 +586,14 @@ class CombineLabels(bpy.types.Operator):
"BioxelNodes_MaskByLabel",
combined_layer_name)
bpy.context.view_layer.objects.active = container
select_object(container)
return {'FINISHED'}
class ConvertToMesh(bpy.types.Operator):
bl_idname = "bioxelnodes.convert_to_mesh"
bl_label = "Convert Container To Mesh"
bl_description = "Convert Bioxel Container To Mesh"
bl_label = "Convert To Mesh"
bl_description = "Convert Container To Mesh"
bl_options = {'UNDO'}
@classmethod
@@ -633,16 +634,147 @@ class ConvertToMesh(bpy.types.Operator):
# constraint=mesh.constraints[0].name, owner='OBJECT')
bpy.ops.object.modifier_apply(modifier=mesh.modifiers[0].name)
bpy.context.object.active_material_index = 1
bpy.ops.object.material_slot_remove()
bpy.context.view_layer.objects.active = mesh
select_object(mesh)
self.report({"INFO"}, f"Successfully convert to mesh")
return {'FINISHED'}
class PickMesh(bpy.types.Operator):
bl_idname = "bioxelnodes.pick_mesh"
bl_label = "Pick Mesh"
bl_description = "Pick Container Mesh"
bl_options = {'UNDO'}
@classmethod
def poll(cls, context):
containers = get_container_from_selection()
return len(containers) > 0
def execute(self, context):
containers = get_container_from_selection()
if len(containers) == 0:
self.report({"WARNING"}, "Cannot find any bioxel container.")
return {'FINISHED'}
container = containers[0]
bpy.ops.mesh.primitive_cube_add(
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
mesh = bpy.context.active_object
mesh.name = f"Mesh_{container.name}"
bpy.ops.node.new_geometry_nodes_modifier()
modifier = mesh.modifiers[0]
node_group = modifier.node_group
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
pick_mesh_node = custom_nodes.add_node(node_group,
"BioxelNodes_PickMesh")
pick_mesh_node.inputs[0].default_value = container
node_group.links.new(pick_mesh_node.outputs[0], output_node.inputs[0])
select_object(mesh)
self.report({"INFO"}, f"Successfully picked mesh")
return {'FINISHED'}
class PickVolume(bpy.types.Operator):
bl_idname = "bioxelnodes.pick_volume"
bl_label = "Pick Volume"
bl_description = "Pick Container Volume"
bl_options = {'UNDO'}
@classmethod
def poll(cls, context):
containers = get_container_from_selection()
return len(containers) > 0
def execute(self, context):
containers = get_container_from_selection()
if len(containers) == 0:
self.report({"WARNING"}, "Cannot find any bioxel container.")
return {'FINISHED'}
container = containers[0]
bpy.ops.mesh.primitive_cube_add(
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
volume = bpy.context.active_object
volume.name = f"Volume_{container.name}"
bpy.ops.node.new_geometry_nodes_modifier()
modifier = volume.modifiers[0]
node_group = modifier.node_group
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
pick_volume_node = custom_nodes.add_node(node_group,
"BioxelNodes_PickVolume")
pick_volume_node.inputs[0].default_value = container
node_group.links.new(
pick_volume_node.outputs[0], output_node.inputs[0])
select_object(volume)
self.report({"INFO"}, f"Successfully picked volume")
return {'FINISHED'}
class PickBboxWire(bpy.types.Operator):
bl_idname = "bioxelnodes.pick_bbox_wire"
bl_label = "Pick Bbox Wire"
bl_description = "Pick Container Bbox Wire"
bl_options = {'UNDO'}
@classmethod
def poll(cls, context):
containers = get_container_from_selection()
return len(containers) > 0
def execute(self, context):
containers = get_container_from_selection()
if len(containers) == 0:
self.report({"WARNING"}, "Cannot find any bioxel container.")
return {'FINISHED'}
container = containers[0]
bpy.ops.mesh.primitive_cube_add(
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
bbox_wire = bpy.context.active_object
bbox_wire.name = f"Wire_{container.name}"
bpy.ops.node.new_geometry_nodes_modifier()
modifier = bbox_wire.modifiers[0]
node_group = modifier.node_group
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
pick_bbox_wire_node = custom_nodes.add_node(node_group,
"BioxelNodes_PickBboxWire")
pick_bbox_wire_node.inputs[0].default_value = container
node_group.links.new(
pick_bbox_wire_node.outputs[0], output_node.inputs[0])
select_object(bbox_wire)
self.report({"INFO"}, f"Successfully picked bbox wire")
return {'FINISHED'}
class AddCutter():
bl_options = {'UNDO'}
@@ -677,6 +809,40 @@ class AddCutter():
node_type = "BioxelNodes_SphereObjectCutter"
bpy.ops.mesh.primitive_ico_sphere_add(
radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
elif self.object_type == "pie":
node_type = "BioxelNodes_PieObjectCutter"
# Create mesh
pie_mesh = bpy.data.meshes.new('Pie')
# Create object
pie = bpy.data.objects.new('Pie', pie_mesh)
# Link object to scene
bpy.context.scene.collection.objects.link(pie)
# Get a BMesh representation
bm = bmesh.new() # create an empty BMesh
bm.from_mesh(pie_mesh) # fill it in from a Mesh
# Hot to create vertices
v_0 = bm.verts.new((0.0, -1.0, 0.0))
v_1 = bm.verts.new((-1.0, -1.0, 1.0))
v_2 = bm.verts.new((0.0, 1.0, 0.0))
v_3 = bm.verts.new((-1.0, 1.0, 1.0))
v_4 = bm.verts.new((1.0, -1.0, 1.0))
v_5 = bm.verts.new((1.0, 1.0, 1.0))
# Initialize the index values of this sequence.
bm.verts.index_update()
# How to create a face
# it's not necessary to create the edges before, I made it only to show how create
# edges too
bm.faces.new((v_0, v_1, v_3, v_2))
bm.faces.new((v_0, v_2, v_5, v_4))
# Finish up, write the bmesh back to the mesh
bm.to_mesh(pie_mesh)
bpy.context.view_layer.objects.active = pie
cutter = bpy.context.active_object
cutter.visible_camera = False
@@ -692,7 +858,7 @@ class AddCutter():
node_group = modifier.node_group
cutter_node = custom_nodes.add_node(node_group, node_type)
cutter_node.inputs[0].default_value = cutter
cut_nodes = get_nodes_by_type(node_group,
'BioxelNodes_Cut')
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
@@ -715,9 +881,10 @@ class AddCutter():
cut_node.inputs[1])
move_node_to_node(cutter_node, cut_node, (-300, -300))
select_object(cutter)
else:
move_node_to_node(cutter_node, output_node, (0, -100))
bpy.context.view_layer.objects.active = container
select_object(container)
return {'FINISHED'}
@@ -752,3 +919,11 @@ class AddSphereCutter(bpy.types.Operator, AddCutter):
bl_description = "Add a Sphere Cutter to Container"
bl_options = {'UNDO'}
object_type = "sphere"
class AddPieCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_pie_cutter"
bl_label = "Add a Pie Cutter"
bl_description = "Add a Pie Cutter to Container"
bl_options = {'UNDO'}
object_type = "pie"

View File

@@ -1,7 +1,8 @@
import re
import bpy
from pathlib import Path
import shutil
from .utils import get_all_layers, get_container, get_container_layers
from .utils import copy_to_dir, get_all_layers, get_container, get_container_from_selection, get_container_layers
from .nodes import custom_nodes
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
@@ -26,18 +27,18 @@ class ReLinkNodes(bpy.types.Operator):
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"
class SaveStagedData(bpy.types.Operator):
bl_idname = "bioxelnodes.save_staged_data"
bl_label = "Save Staged Data"
bl_description = "Save all staged data in this file for sharing"
save_layer: bpy.props.BoolProperty(
name="Save Layer VDB Cache",
default=True,
) # type: ignore
layer_dir: bpy.props.StringProperty(
name="Layer Directory",
cache_dir: bpy.props.StringProperty(
name="Cache Directory",
subtype='DIR_PATH',
default="//"
) # type: ignore
@@ -83,15 +84,18 @@ class SaveAllToShare(bpy.types.Operator):
self.report({"INFO"}, f"Successfully saved to {output_path}")
if self.save_layer:
layers = get_all_layers()
for layer in layers:
fails = []
for layer in get_all_layers():
try:
save_layer(layer, self.layer_dir)
save_layer(layer, self.cache_dir)
except:
self.report(
{"WARNING"}, f"Fail to save {layer.name}, skiped")
fails.append(layer)
self.report({"INFO"}, f"Successfully saved bioxel layers.")
if len(fails) == 0:
self.report({"INFO"}, f"Successfully saved bioxel layers.")
else:
self.report(
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
return {'FINISHED'}
@@ -108,36 +112,43 @@ class SaveAllToShare(bpy.types.Operator):
layout = self.layout
panel = layout.box()
panel.prop(self, "save_layer")
panel.prop(self, "layer_dir")
panel.prop(self, "cache_dir")
panel = layout.box()
panel.prop(self, "save_lib")
panel.prop(self, "lib_dir")
def save_layer(layer, output_dir):
name = layer.name
pattern = r'\.\d{4}\.'
# "//"
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()
is_sequence = re.search(pattern, source_path.name) is not None
name = layer.name if is_sequence else f"{layer.name}.vdb"
output_path: Path = Path(output_dir, name, source_path.name).resolve() \
if is_sequence else Path(output_dir, name).resolve()
if output_path != source_path:
shutil.copy(source_path, output_path)
copy_to_dir(source_path.parent if is_sequence else source_path,
output_path.parent.parent if is_sequence else output_path.parent,
new_name=name)
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."
bl_label = "Save Layers"
bl_description = "Save Container Layers to Directory."
layer_dir: bpy.props.StringProperty(
cache_dir: bpy.props.StringProperty(
name="Layer Directory",
subtype='DIR_PATH',
default="//"
@@ -145,24 +156,29 @@ class SaveLayers(bpy.types.Operator):
@classmethod
def poll(cls, context):
container = get_container(bpy.context.active_object)
return True if container else False
containers = get_container_from_selection()
return len(containers) > 0
def execute(self, context):
container = get_container(bpy.context.active_object)
containers = get_container_from_selection()
if not container:
if len(containers) == 0:
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")
fails = []
for container in containers:
for layer in get_container_layers(container):
try:
save_layer(layer, self.cache_dir)
except:
fails.append(layer)
self.report({"INFO"}, f"Successfully saved bioxel layers.")
if len(fails) == 0:
self.report({"INFO"}, f"Successfully saved bioxel layers.")
else:
self.report(
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
return {'FINISHED'}
@@ -170,3 +186,27 @@ class SaveLayers(bpy.types.Operator):
context.window_manager.invoke_props_dialog(self,
width=500)
return {'RUNNING_MODAL'}
class CleanAllCaches(bpy.types.Operator):
bl_idname = "bioxelnodes.clear_all_caches"
bl_label = "Clean All Caches in Temp"
bl_description = "Clean all caches saved in temp"
def execute(self, context):
preferences = context.preferences.addons[__package__].preferences
cache_dir = Path(preferences.cache_dir, 'VDBs')
try:
shutil.rmtree(cache_dir)
self.report({"INFO"}, f"Successfully cleaned caches.")
return {'FINISHED'}
except:
self.report({"WARNING"},
"Fail to clean caches, you may do it manually.")
return {'CANCELLED'}
def invoke(self, context, event):
context.window_manager.invoke_confirm(self,
event,
message="All caches will be cleaned, include other project files, do you still want to clean?")
return {'RUNNING_MODAL'}

View File

@@ -3,6 +3,45 @@ import mathutils
from pathlib import Path
import pyopenvdb as vdb
from uuid import uuid4
import shutil
def select_object(target_obj):
for obj in bpy.data.objects:
obj.select_set(False)
target_obj.select_set(True)
bpy.context.view_layer.objects.active = target_obj
def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
source = Path(source_path)
target = Path(dir_path)
# Check if the source exists
if not source.exists():
raise FileNotFoundError
# Check if the target exists
if not target.exists():
target.mkdir(parents=True, exist_ok=True)
target_path = target / new_name if new_name else target / source.name
# If source is a file, copy it to the target directory
if source.is_file():
try:
shutil.copy(source, target_path)
except shutil.SameFileError:
if exist_ok:
pass
else:
raise shutil.SameFileError
# If source is a directory, copy its contents to the target directory
elif source.is_dir():
shutil.copytree(source, target_path, dirs_exist_ok=exist_ok)
if not target_path.exists():
raise Exception
def move_node_to_node(node, target_node, offset=(0, 0)):
@@ -124,13 +163,35 @@ def save_vdb(grids, context):
return vdb_path
def save_vdbs(grids_sequence, context):
preferences = context.preferences.addons[__package__].preferences
cache_dir = Path(preferences.cache_dir, 'VDBs')
cache_dir.mkdir(parents=True, exist_ok=True)
vdb_name = str(uuid4())
vdb_dir_path = Path(cache_dir, vdb_name)
vdb_dir_path.mkdir(parents=True, exist_ok=True)
vdb_paths = []
for f, grids in enumerate(grids_sequence):
vdb_path = Path(vdb_dir_path, f"{vdb_name}.{str(f+1).zfill(4)}.vdb")
print(f"Storing the VDB file ({str(vdb_path)})...")
vdb.write(str(vdb_path), grids=grids)
vdb_paths.append(vdb_path)
return vdb_paths
def get_container_from_selection():
containers = []
for obj in bpy.context.selected_objects:
if get_container(obj):
containers.append(obj)
# if bpy.context.active_object:
# if bpy.context.active_object.get('bioxel_layer'):
# if bpy.context.active_object.parent.get('bioxel_container'):
# containers.append(bpy.context.active_object.parent)
return containers
return list(set(containers))
def get_container(current_obj):