Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
467d368a76 | ||
| 134d8e382b | |||
|
|
fefc9285e1 | ||
| 322b84cc70 | |||
| f3be5d8660 | |||
| 470cdab4f6 |
37
.github/workflows/upload-assets.yml
vendored
37
.github/workflows/upload-assets.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
# id: draft_release
|
||||
# uses: cardinalby/git-get-release-action@1.2.4
|
||||
# with:
|
||||
# releaseName: Draft
|
||||
# releaseName: Upload
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -70,5 +70,38 @@ jobs:
|
||||
with:
|
||||
upload_url: ${{ needs.draft_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
asset_path: ./package.zip
|
||||
asset_name: BioxelNodes_${{ needs.draft_release.outputs.version }}.zip
|
||||
asset_name: BioxelNodes_Addon_${{ needs.draft_release.outputs.version }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
upload_blender_extension:
|
||||
name: Upload Blender Extension
|
||||
needs: draft_release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: 'true'
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Zip Extension
|
||||
run: |
|
||||
pip download SimpleITK --dest bioxelnodes/wheels --only-binary=:all: --python-version=3.11 --platform=macosx_11_0_arm64
|
||||
pip download SimpleITK --dest bioxelnodes/wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64
|
||||
rm -r bioxelnodes/externalpackage
|
||||
cp extension/__init__.py bioxelnodes/__init__.py
|
||||
cp extension/preferences.py bioxelnodes/preferences.py
|
||||
cp extension/blender_manifest.toml bioxelnodes/blender_manifest.toml
|
||||
zip -r package.zip bioxelnodes
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.draft_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
asset_path: ./package.zip
|
||||
asset_name: BioxelNodes_Extension_${{ needs.draft_release.outputs.version }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
14
README.md
14
README.md
@@ -15,7 +15,7 @@ Before us, there have been many tutorials and add-ons for importing volumetric d
|
||||
|
||||
Below are some examples with Bioxel Nodes. Thanks to Cycles Render, the volumetric data can be rendered with great detail:
|
||||
|
||||

|
||||

|
||||
|
||||
The "Bioxel" in "Bioxel Nodes", is a combination of the words "Bio-" and "Voxel". Bioxel is a voxel that stores biological data. We are developing a toolkit around Bioxel 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. [Getting Started](https://omoolab.github.io/BioxelNodes/latest/getting-started)
|
||||
|
||||
@@ -56,3 +56,15 @@ It is not support editing the same blender file across add-on versions. In order
|
||||
But even then, there is still no guarantee that the new version of the add-on will work on the old blender file. Therefore, it is highly recommended to open a new blender file to start the creating, not based on the old one.
|
||||
|
||||
Alternatively, objects from the old file that have nothing to do with Bioxel Nodes could be append to the new blender file.
|
||||
|
||||
## About EEVEE Render
|
||||
|
||||
Bioxel Nodes is designed for Cycles Render. However, it does support eevee render partially. "Solid Shader" node and "Volume Shader" node have a toggle called "EEVEE Render". If you want to render Bioxel Component in real-time, turn it on.
|
||||
|
||||
Also, there are some limitations:
|
||||
|
||||
1. Only one cutter supported.
|
||||
2. You cannot use "Color Ramp" over 2 colors.
|
||||
3. EEVEE Render result is not that great as Cycles does.
|
||||
|
||||
> "Volume Shader" node is not work properly in EEVEE Next since 4.2. It is because EEVEE Next is not support attributes from instances of volume shader by now. But the Blender 4.2 docs still say attributes reading is ok, so I suppose this feature will eventually be implemented.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -57,7 +57,7 @@ def unregister():
|
||||
#################################################
|
||||
|
||||
def get_all_submodules(directory):
|
||||
return list(iter_submodules(directory, directory.name))
|
||||
return list(iter_submodules(directory, __package__))
|
||||
|
||||
|
||||
def iter_submodules(path, package_name):
|
||||
|
||||
@@ -76,7 +76,8 @@ class PackageInstaller():
|
||||
|
||||
# Set up logging configuration
|
||||
logfile_path = Path(self.log_path, f"{logfile_name}.log")
|
||||
logging.basicConfig(filename=logfile_path, level=logging.INFO)
|
||||
logging.basicConfig(filename=logfile_path,
|
||||
level=logging.INFO, encoding='utf-8')
|
||||
|
||||
# Return logger object
|
||||
return logging.getLogger()
|
||||
@@ -248,13 +249,16 @@ class PackageInstaller():
|
||||
result = subprocess.run(cmd_list, timeout=timeout,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
if result.returncode != 0:
|
||||
log.error('Command failed: %s', cmd_list)
|
||||
log.error('stdout: %s', result.stdout.decode())
|
||||
log.error('stderr: %s', result.stderr.decode(errors='ignore'))
|
||||
else:
|
||||
log.info('Command succeeded: %s', cmd_list)
|
||||
log.info('stdout: %s', result.stdout.decode())
|
||||
try:
|
||||
if result.returncode != 0:
|
||||
log.error('Command failed: %s', cmd_list)
|
||||
log.error('stdout: %s', result.stdout.decode())
|
||||
log.error('stderr: %s', result.stderr.decode(errors='ignore'))
|
||||
else:
|
||||
log.info('Command succeeded: %s', cmd_list)
|
||||
log.info('stdout: %s', result.stdout.decode())
|
||||
except:
|
||||
...
|
||||
# return the command list, return code, stdout, and stderr as a tuple
|
||||
return result
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ import numpy as np
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
import mathutils
|
||||
import random
|
||||
|
||||
from .nodes import custom_nodes
|
||||
from .props import BIOXELNODES_Series
|
||||
from .utils import (calc_bbox_verts, get_container, get_layer, get_text_index_str,
|
||||
from .utils import (calc_bbox_verts, get_all_layers, get_container, get_layer, get_text_index_str,
|
||||
get_node_by_type, hide_in_ray, lock_transform, show_message)
|
||||
|
||||
try:
|
||||
@@ -218,6 +219,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
is_first_import = len(get_all_layers()) == 0
|
||||
image, name = read_image(self.filepath, self.series_id)
|
||||
container_name = self.container_name or name
|
||||
bioxel_size = self.bioxel_size
|
||||
@@ -258,7 +260,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
interpolator = sitk.sitkNearestNeighbor
|
||||
elif self.resample_method == "gaussian":
|
||||
interpolator = sitk.sitkGaussian
|
||||
|
||||
|
||||
if self.read_as == "labels":
|
||||
interpolator = sitk.sitkNearestNeighbor
|
||||
|
||||
@@ -451,11 +453,14 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
input_node = get_node_by_type(nodes, 'NodeGroupInput')[0]
|
||||
output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0]
|
||||
|
||||
to_layer_node = custom_nodes.add_node(nodes, "BioxelNodes__ConvertToLayer")
|
||||
to_layer_node = custom_nodes.add_node(nodes,
|
||||
"BioxelNodes__ConvertToLayer")
|
||||
|
||||
links.new(input_node.outputs[0], to_layer_node.inputs[0])
|
||||
links.new(to_layer_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
to_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
|
||||
200000000)
|
||||
to_layer_node.inputs['Bioxel Size'].default_value = bioxel_size
|
||||
to_layer_node.inputs['Shape'].default_value = layer_shape
|
||||
to_layer_node.inputs['Origin'].default_value = layer_origin
|
||||
@@ -531,7 +536,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
bpy.context.view_layer.objects.active = container
|
||||
|
||||
# Change render setting for better result
|
||||
if preferences.do_change_render_setting:
|
||||
if preferences.do_change_render_setting and is_first_import:
|
||||
bpy.context.scene.render.engine = 'CYCLES'
|
||||
bpy.context.scene.cycles.volume_bounces = 12
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||
|
||||
@@ -22,200 +22,4 @@ def create_points_obj(points, name="points"):
|
||||
# make the bmesh the object's mesh
|
||||
bm.to_mesh(mesh)
|
||||
bm.free() # always do this when finished
|
||||
return obj
|
||||
|
||||
|
||||
# class ImportDICOMPointsDialog(bpy.types.Operator):
|
||||
# bl_idname = "bioxelnodes.import_dicom_points_dialog"
|
||||
# bl_label = "Volume Data as Bioxels"
|
||||
# bl_description = "Import Volume Data as Bioxels (VDB)"
|
||||
# bl_options = {'UNDO'}
|
||||
|
||||
# filepath: bpy.props.StringProperty(
|
||||
# subtype="FILE_PATH",
|
||||
# options={'HIDDEN'}
|
||||
# ) # type: ignore
|
||||
|
||||
# bioxels_shape: bpy.props.IntVectorProperty(
|
||||
# name="Bioxels Shape (ReadOnly)",
|
||||
# min=0,
|
||||
# default=(100, 100, 100)
|
||||
# ) # type: ignore
|
||||
|
||||
# bioxel_size: bpy.props.FloatProperty(
|
||||
# name="Bioxel Size",
|
||||
# soft_min=0.1, soft_max=10.0,
|
||||
# min=1e-2, max=1e2,
|
||||
# default=1,
|
||||
# update=on_bioxel_size_changed
|
||||
# ) # type: ignore
|
||||
|
||||
# orig_spacing: bpy.props.FloatVectorProperty(
|
||||
# name="Original Spacing",
|
||||
# default=(1, 1, 1),
|
||||
# update=on_orig_spacing_changed
|
||||
# ) # type: ignore
|
||||
|
||||
# orig_shape: bpy.props.IntVectorProperty(
|
||||
# name="Original Shape",
|
||||
# default=(100, 100, 100),
|
||||
# options={'HIDDEN'}
|
||||
# ) # type: ignore
|
||||
|
||||
# auto: bpy.props.BoolProperty(
|
||||
# name="Auto Setting",
|
||||
# default=False,
|
||||
# options={'HIDDEN'}
|
||||
# ) # type: ignore
|
||||
|
||||
# scene_scale: bpy.props.FloatProperty(
|
||||
# name="Scene Scale",
|
||||
# soft_min=0.001, soft_max=100.0,
|
||||
# min=1e-6, max=1e6,
|
||||
# default=0.01,
|
||||
# ) # type: ignore
|
||||
|
||||
# do_add_segmentnode: bpy.props.BoolProperty(
|
||||
# name="Add Segment Node",
|
||||
# default=True,
|
||||
# ) # type: ignore
|
||||
|
||||
# do_change_render_setting: bpy.props.BoolProperty(
|
||||
# name="Change Render Setting",
|
||||
# default=True,
|
||||
# ) # type: ignore
|
||||
|
||||
# def execute(self, context):
|
||||
# files = get_data_files(self.filepath)
|
||||
# name = Path(self.filepath).parent.name
|
||||
|
||||
# image = sitk.ReadImage(files)
|
||||
|
||||
# bioxel_size = float(self.bioxel_size)
|
||||
# orig_spacing = tuple(self.orig_spacing)
|
||||
# image_spacing = image.GetSpacing()
|
||||
# image_shape = image.GetSize()
|
||||
|
||||
# bioxels_spacing = (
|
||||
# image_spacing[0] / orig_spacing[0] * bioxel_size,
|
||||
# image_spacing[1] / orig_spacing[1] * bioxel_size,
|
||||
# image_spacing[2] / orig_spacing[2] * bioxel_size
|
||||
# )
|
||||
|
||||
# bioxels_shape = (
|
||||
# int(image_shape[0] / bioxel_size * orig_spacing[0]),
|
||||
# int(image_shape[1] / bioxel_size * orig_spacing[1]),
|
||||
# int(image_shape[2] / bioxel_size * orig_spacing[2]),
|
||||
# )
|
||||
|
||||
# print("Resampling...")
|
||||
# image = sitk.Resample(
|
||||
# image1=image,
|
||||
# size=bioxels_shape,
|
||||
# transform=sitk.Transform(),
|
||||
# interpolator=sitk.sitkLinear,
|
||||
# outputOrigin=image.GetOrigin(),
|
||||
# outputSpacing=bioxels_spacing,
|
||||
# outputDirection=image.GetDirection(),
|
||||
# defaultPixelValue=0,
|
||||
# outputPixelType=image.GetPixelID(),
|
||||
# )
|
||||
|
||||
# print("Orienting to RAS...")
|
||||
# image = sitk.DICOMOrient(image, 'RAS')
|
||||
|
||||
# array = sitk.GetArrayFromImage(image)
|
||||
# orig_dtype = str(array.dtype)
|
||||
# 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]
|
||||
# # https://www.slicer.org/wiki/Coordinate_systems
|
||||
|
||||
# # ITK Numpy 3D
|
||||
# # R (ight) i -> k -> x
|
||||
# # A (nterior) j -> j -> y
|
||||
# # S (uperior) k -> i -> z
|
||||
|
||||
# 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":
|
||||
# bioxels_offset = -bioxels_min
|
||||
# array = array + np.full_like(array, bioxels_offset)
|
||||
# bioxels_max = float(np.max(array))
|
||||
# bioxels_min = float(np.min(array))
|
||||
# print("Offseted Max:", bioxels_max)
|
||||
# print("Offseted Min:", bioxels_min)
|
||||
|
||||
# points = []
|
||||
# for x in range(array.shape[0]):
|
||||
# for y in range(array.shape[1]):
|
||||
# for z in range(array.shape[2]):
|
||||
# points.append({
|
||||
# "pos": (x, y, z),
|
||||
# "value": array[x, y, z],
|
||||
# })
|
||||
|
||||
# # # Build VDB
|
||||
# # grid = vdb.FloatGrid()
|
||||
# # grid.copyFromArray(array.copy())
|
||||
|
||||
# # # After sitk.DICOMOrient(), origin and direction will also orient base on LPS
|
||||
# # # so we need to convert them into RAS
|
||||
# # mat_lps2ras = axis_conversion(
|
||||
# # from_forward='-Z',
|
||||
# # from_up='-Y',
|
||||
# # to_forward='-Z',
|
||||
# # to_up='Y'
|
||||
# # ).to_4x4()
|
||||
|
||||
# # mat_location = mathutils.Matrix.Translation(
|
||||
# # mathutils.Vector(image.GetOrigin())
|
||||
# # )
|
||||
|
||||
# # mat_rotation = mathutils.Matrix(
|
||||
# # np.array(image.GetDirection()).reshape((3, 3))
|
||||
# # ).to_4x4()
|
||||
|
||||
# # mat_scale = mathutils.Matrix.Scale(
|
||||
# # bioxel_size, 4
|
||||
# # )
|
||||
|
||||
# # transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale
|
||||
|
||||
# obj = create_points_obj(points)
|
||||
|
||||
# # Make transformation
|
||||
# scene_scale = float(self.scene_scale)
|
||||
# # (S)uperior -Z -> Y
|
||||
# # (A)osterior Y -> Z
|
||||
# mat_ras2blender = axis_conversion(
|
||||
# from_forward='-Z',
|
||||
# from_up='Y',
|
||||
# to_forward='Y',
|
||||
# to_up='Z'
|
||||
# ).to_4x4()
|
||||
|
||||
# mat_scene_scale = mathutils.Matrix.Scale(
|
||||
# scene_scale, 4
|
||||
# )
|
||||
|
||||
# obj.matrix_world = mat_ras2blender @ mat_scene_scale
|
||||
|
||||
# self.report({"INFO"}, "Successfully Imported")
|
||||
|
||||
# return {'FINISHED'}
|
||||
|
||||
# def invoke(self, context, event):
|
||||
# # print(tuple(self.orig_shape))
|
||||
# self.auto = True
|
||||
# self.bioxel_size = 1
|
||||
# context.window_manager.invoke_props_dialog(self)
|
||||
# return {'RUNNING_MODAL'}
|
||||
return obj
|
||||
@@ -1,7 +1,7 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from bioxelnodes.utils import get_all_layers, get_container, get_container_layers
|
||||
from .utils import get_all_layers, get_container, get_container_layers
|
||||
|
||||
|
||||
def save_layer(layer, output_dir):
|
||||
|
||||
@@ -7,7 +7,7 @@ import bpy
|
||||
def add_driver_to_node_factory(source_prop, target_prop):
|
||||
callback_str = f"""
|
||||
import bpy
|
||||
from bioxelnodes.utils import add_direct_driver, get_bioxels_obj
|
||||
from .utils import add_direct_driver, get_bioxels_obj
|
||||
bioxels_obj = get_bioxels_obj(bpy.context.active_object)
|
||||
if bioxels_obj:
|
||||
container_obj = bioxels_obj.parent
|
||||
@@ -26,7 +26,7 @@ else:
|
||||
def set_prop_to_node_factory(source_prop, target_prop):
|
||||
callback_str = f"""
|
||||
import bpy
|
||||
from bioxelnodes.utils import get_bioxels_obj
|
||||
from .utils import get_bioxels_obj
|
||||
bioxels_obj = get_bioxels_obj(bpy.context.active_object)
|
||||
if bioxels_obj:
|
||||
container_obj = bioxels_obj.parent
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
import random
|
||||
import bpy
|
||||
import pyopenvdb as vdb
|
||||
from uuid import uuid4
|
||||
@@ -107,45 +108,6 @@ class JoinLayers(bpy.types.Operator):
|
||||
filepath=str(vdb_path), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
|
||||
joined_layer = bpy.context.active_object
|
||||
|
||||
base_layer_node = base_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
||||
bioxel_size = base_layer_node.inputs['Bioxel Size'].default_value
|
||||
layer_shape = base_layer_node.inputs['Shape'].default_value
|
||||
layer_origin = base_layer_node.inputs['Origin'].default_value
|
||||
layer_rotation = base_layer_node.inputs['Rotation'].default_value
|
||||
scalar_offset = base_layer_node.inputs['Scalar Offset'].default_value
|
||||
scalar_min = base_layer_node.inputs['Scalar Min'].default_value
|
||||
scalar_max = base_layer_node.inputs['Scalar Max'].default_value
|
||||
|
||||
if self.scalar_layer != "None":
|
||||
scalar_layer_node = scalar_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
||||
scalar_offset = scalar_layer_node.inputs['Scalar Offset'].default_value
|
||||
scalar_min = scalar_layer_node.inputs['Scalar Min'].default_value
|
||||
scalar_max = scalar_layer_node.inputs['Scalar Max'].default_value
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
node_tree = joined_layer.modifiers[0].node_group
|
||||
nodes = node_tree.nodes
|
||||
links = node_tree.links
|
||||
|
||||
input_node = get_node_by_type(nodes, 'NodeGroupInput')[0]
|
||||
output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0]
|
||||
|
||||
|
||||
joined_layer_node = custom_nodes.add_node(
|
||||
nodes, "BioxelNodes__ConvertToLayer")
|
||||
|
||||
|
||||
links.new(input_node.outputs[0], joined_layer_node.inputs[0])
|
||||
links.new(joined_layer_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
joined_layer_node.inputs['Bioxel Size'].default_value = bioxel_size
|
||||
joined_layer_node.inputs['Shape'].default_value = layer_shape
|
||||
joined_layer_node.inputs['Origin'].default_value = layer_origin
|
||||
joined_layer_node.inputs['Rotation'].default_value = layer_rotation
|
||||
joined_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
|
||||
joined_layer_node.inputs['Scalar Min'].default_value = scalar_min
|
||||
joined_layer_node.inputs['Scalar Max'].default_value = scalar_max
|
||||
|
||||
# Set props to VDB object
|
||||
joined_layer.name = f"{base_layer.name}_Joined"
|
||||
@@ -162,6 +124,52 @@ class JoinLayers(bpy.types.Operator):
|
||||
joined_layer['bioxel_layer'] = True
|
||||
joined_layer.parent = base_layer.parent
|
||||
|
||||
for collection in layer.users_collection:
|
||||
collection.objects.unlink(layer)
|
||||
|
||||
for collection in base_layer.users_collection:
|
||||
collection.objects.link(layer)
|
||||
|
||||
# add convert to layer node
|
||||
base_layer_node = base_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
||||
bioxel_size = base_layer_node.inputs['Bioxel Size'].default_value
|
||||
layer_shape = base_layer_node.inputs['Shape'].default_value
|
||||
layer_origin = base_layer_node.inputs['Origin'].default_value
|
||||
layer_rotation = base_layer_node.inputs['Rotation'].default_value
|
||||
scalar_offset = base_layer_node.inputs['Scalar Offset'].default_value
|
||||
scalar_min = base_layer_node.inputs['Scalar Min'].default_value
|
||||
scalar_max = base_layer_node.inputs['Scalar Max'].default_value
|
||||
|
||||
if self.scalar_layer != "None":
|
||||
scalar_layer_node = scalar_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
||||
scalar_offset = scalar_layer_node.inputs['Scalar Offset'].default_value
|
||||
scalar_min = scalar_layer_node.inputs['Scalar Min'].default_value
|
||||
scalar_max = scalar_layer_node.inputs['Scalar Max'].default_value
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
node_tree = joined_layer.modifiers[0].node_group
|
||||
nodes = node_tree.nodes
|
||||
links = node_tree.links
|
||||
|
||||
input_node = get_node_by_type(nodes, 'NodeGroupInput')[0]
|
||||
output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0]
|
||||
|
||||
joined_layer_node = custom_nodes.add_node(
|
||||
nodes, "BioxelNodes__ConvertToLayer")
|
||||
|
||||
links.new(input_node.outputs[0], joined_layer_node.inputs[0])
|
||||
links.new(joined_layer_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
joined_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
|
||||
200000000)
|
||||
joined_layer_node.inputs['Bioxel Size'].default_value = bioxel_size
|
||||
joined_layer_node.inputs['Shape'].default_value = layer_shape
|
||||
joined_layer_node.inputs['Origin'].default_value = layer_origin
|
||||
joined_layer_node.inputs['Rotation'].default_value = layer_rotation
|
||||
joined_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
|
||||
joined_layer_node.inputs['Scalar Min'].default_value = scalar_min
|
||||
joined_layer_node.inputs['Scalar Max'].default_value = scalar_max
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -206,8 +214,8 @@ class ConvertToMesh(bpy.types.Operator):
|
||||
output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0]
|
||||
object_node = nodes.new("GeometryNodeObjectInfo")
|
||||
realize_nodes = nodes.new("GeometryNodeRealizeInstances")
|
||||
separate_node = custom_nodes.add_node(
|
||||
nodes, "BioxelNodes__SeparateComponent")
|
||||
separate_node = custom_nodes.add_node(nodes,
|
||||
"BioxelNodes__SeparateComponent")
|
||||
|
||||
object_node.inputs[0].default_value = container
|
||||
separate_node.inputs[1].default_value = 1
|
||||
|
||||
@@ -26,5 +26,4 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferen
|
||||
|
||||
layout.label(text="Configuration")
|
||||
layout.prop(self, 'cache_dir')
|
||||
|
||||
layout.prop(self, "do_change_render_setting")
|
||||
|
||||
BIN
docs/assets/cover.png
Normal file
BIN
docs/assets/cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -4,8 +4,19 @@ Currently only support Blender 4.0 or above, make sure you have the correct vers
|
||||
|
||||
## 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.
|
||||
#### For Blender 4.2 or higher
|
||||
|
||||
Download the **Extension** version `BioxelNodes_Extension_{version}.zip` from https://github.com/OmooLab/BioxelNodes/releases/latest
|
||||
In Blender, Edit > Preferences > Extensions > Install from Disk, select the zip file you just downloaded.
|
||||
|
||||
Thats it!
|
||||
|
||||
> If it cannot be enable, just reboot blender.
|
||||
|
||||
#### For Blender 4.0 or 4.1
|
||||
|
||||
Download the **Addon** version `BioxelNodes_Addon_{version}.zip` from https://github.com/OmooLab/BioxelNodes/releases/latest
|
||||
In Blender, Edit > Preferences > Add-ons > Install, select the zip file you just downloaded.
|
||||
|
||||
The add-on requires a third-party python dependency called SimpleITK, click `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 that, click `Reboot Blender` button.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Before us, there have been many tutorials and add-ons for importing volumetric d
|
||||
|
||||
Below are some examples with Bioxel Nodes. Thanks to Cycles Render, the volumetric data can be rendered with great detail:
|
||||
|
||||

|
||||

|
||||
|
||||
The "Bioxel" in "Bioxel Nodes", is a combination of the words "Bio-" and "Voxel". Bioxel is a voxel that stores biological data. We are developing a toolkit around Bioxel 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. [Getting Started](https://omoolab.github.io/BioxelNodes/latest/getting-started)
|
||||
|
||||
@@ -54,3 +54,15 @@ It is not support editing the same blender file across add-on versions. In order
|
||||
But even then, there is still no guarantee that the new version of the add-on will work on the old blender file. Therefore, it is highly recommended to open a new blender file to start the creating, not based on the old one.
|
||||
|
||||
Alternatively, objects from the old file that have nothing to do with Bioxel Nodes could be append to the new blender file.
|
||||
|
||||
## About EEVEE Render
|
||||
|
||||
Bioxel Nodes is designed for Cycles Render. However, it does support eevee render partially. "Solid Shader" node and "Volume Shader" node have a toggle called "EEVEE Render". If you want to render Bioxel Component in real-time, turn it on.
|
||||
|
||||
Also, there are some limitations:
|
||||
|
||||
1. Only one cutter supported.
|
||||
2. You cannot use "Color Ramp" over 2 colors.
|
||||
3. EEVEE Render result is not that great as Cycles does.
|
||||
|
||||
> "Volume Shader" node is not work properly in EEVEE Next since 4.2. It is because EEVEE Next is not support attributes from instances of volume shader by now. But the Blender 4.2 docs still say attributes reading is ok, so I suppose this feature will eventually be implemented.
|
||||
|
||||
22
extension/__init__.py
Normal file
22
extension/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import bpy
|
||||
|
||||
from . import auto_load
|
||||
from . import menus
|
||||
|
||||
|
||||
auto_load.init()
|
||||
|
||||
|
||||
def register():
|
||||
auto_load.register()
|
||||
menus.add()
|
||||
bpy.types.Scene.bioxel_layer_dir = bpy.props.StringProperty(
|
||||
name="Bioxel Layers Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
menus.remove()
|
||||
auto_load.unregister()
|
||||
56
extension/blender_manifest.toml
Normal file
56
extension/blender_manifest.toml
Normal file
@@ -0,0 +1,56 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
# Example of manifest file for a Blender extension
|
||||
# Change the values according to your extension
|
||||
id = "bioxelnodes"
|
||||
version = "0.2.2"
|
||||
name = "Bioxel Nodes"
|
||||
tagline = "For scientific volumetric data visualization in Blender"
|
||||
maintainer = "Ma Nan <icrdr2010@outlook.com>"
|
||||
# Supported types: "add-on", "theme"
|
||||
type = "add-on"
|
||||
|
||||
# Optional: add-ons can list which resources they will require:
|
||||
# * "files" (for access of any filesystem operations)
|
||||
# * "network" (for internet access)
|
||||
# * "clipboard" (to read and/or write the system clipboard)
|
||||
# * "camera" (to capture photos and videos)
|
||||
# * "microphone" (to capture audio)
|
||||
permissions = ["files"]
|
||||
|
||||
# Optional link to documentation, support, source files, etc
|
||||
website = "https://omoolab.github.io/BioxelNodes/latest"
|
||||
|
||||
# Optional list defined by Blender and server, see:
|
||||
# https://docs.blender.org/manual/en/dev/extensions/tags.html
|
||||
tags = ["Geometry Nodes", "Render", "Material"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
# Optional: maximum supported Blender version
|
||||
# blender_version_max = "5.1.0"
|
||||
|
||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
||||
# https://docs.blender.org/manual/en/dev/extensions/licenses.html
|
||||
license = ["SPDX:MIT"]
|
||||
# Optional: required by some licenses.
|
||||
copyright = ["2024 OmooLab"]
|
||||
|
||||
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
|
||||
platforms = ["windows-amd64", "macos-x86_64"]
|
||||
# Other supported platforms: "windows-arm64", "macos-x86_64"
|
||||
|
||||
# TODO: externalpackage to wheels
|
||||
# Optional: bundle 3rd party Python modules.
|
||||
# https://docs.blender.org/manual/en/dev/extensions/python_wheels.html
|
||||
wheels = [
|
||||
"./wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl",
|
||||
"./wheels/SimpleITK-2.3.1-cp311-cp311-macosx_11_0_arm64.whl",
|
||||
]
|
||||
|
||||
# Optional: build setting.
|
||||
# https://docs.blender.org/manual/en/dev/extensions/command_line_arguments.html#command-line-args-extension-build
|
||||
# [build]
|
||||
# paths_exclude_pattern = [
|
||||
# "/.git/"
|
||||
# "__pycache__/"
|
||||
# ]
|
||||
23
extension/preferences.py
Normal file
23
extension/preferences.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
|
||||
class BioxelNodesPreferences(bpy.types.AddonPreferences):
|
||||
bl_idname = __package__
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Set Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
default=str(Path(Path.home(), '.bioxelnodes'))
|
||||
) # type: ignore
|
||||
|
||||
do_change_render_setting: bpy.props.BoolProperty(
|
||||
name="Change Render Setting",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.label(text="Configuration")
|
||||
layout.prop(self, 'cache_dir')
|
||||
layout.prop(self, "do_change_render_setting")
|
||||
11
mkdocs.yml
11
mkdocs.yml
@@ -8,11 +8,11 @@ repo_url: https://github.com/OmooLab/BioxelNodes
|
||||
|
||||
nav:
|
||||
- BioxelNodes:
|
||||
- index.md
|
||||
- Getting Started: getting-started.md
|
||||
- Features & Options: features.md
|
||||
- Nodes: nodes.md
|
||||
- Future Features: misc.md
|
||||
- index.md
|
||||
- Getting Started: getting-started.md
|
||||
- Features & Options: features.md
|
||||
- Nodes: nodes.md
|
||||
- Future Features: misc.md
|
||||
|
||||
theme:
|
||||
name: material
|
||||
@@ -57,6 +57,7 @@ theme:
|
||||
extra:
|
||||
version:
|
||||
provider: mike
|
||||
default: latest
|
||||
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "bioxelnodes"
|
||||
version = "0.2.0"
|
||||
version = "0.2.2"
|
||||
description = ""
|
||||
authors = ["Ma Nan <icrdr2010@outlook.com>"]
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user