feat: Add Bioxel asset catalog and layer management functionality
- Introduced a new asset catalog definition file for Blender to manage Bioxel assets. - Implemented layer caching and snapshot functionality in `layer.py`, allowing for VDB file generation and low-resolution snapshots. - Created node management functions in `node.py` for handling Bioxel nodes within Blender's Geometry Node Editor. - Added an operator to extract mesh data from selected nodes in `structure.py`. - Developed UI panels in `panels.py` for layer management, including import options and layer library display.
This commit is contained in:
@@ -1,110 +1,21 @@
|
||||
import json
|
||||
import uuid
|
||||
import webbrowser
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from ..bioxelutils.common import get_all_layer_objs, get_node_lib_path, get_node_version, is_missing_layer, set_file_prop
|
||||
from .layer import RemoveLayers, SaveLayersCache
|
||||
|
||||
from ..constants import LATEST_NODE_LIB_PATH, VERSIONS
|
||||
from ..utils import get_cache_dir
|
||||
|
||||
|
||||
class ReLinkNodeLib(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.relink_node_lib"
|
||||
bl_label = "Relink Node Library"
|
||||
bl_description = "Relink all nodes to addon library source"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
index: bpy.props.IntProperty() # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
node_version = VERSIONS[self.index]['node_version']
|
||||
lib_path = get_node_lib_path(node_version) \
|
||||
if self.index != 0 else LATEST_NODE_LIB_PATH
|
||||
|
||||
node_libs = []
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
node_lib = node_group.library
|
||||
if node_lib:
|
||||
node_libs.append(node_lib)
|
||||
|
||||
node_libs = list(set(node_libs))
|
||||
|
||||
for node_lib in node_libs:
|
||||
node_lib.filepath = str(lib_path)
|
||||
# FIXME: may cause crash
|
||||
node_lib.reload()
|
||||
|
||||
set_file_prop("node_version", node_version)
|
||||
|
||||
self.report({"INFO"}, f"Successfully relinked.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveNodeLib(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_node_lib"
|
||||
bl_label = "Save Node Library"
|
||||
bl_description = "Save node library file to local"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
lib_dir: bpy.props.StringProperty(
|
||||
name="Library Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
node_version = get_node_version()
|
||||
|
||||
if node_version is None:
|
||||
node_version = VERSIONS[0]["node_version"]
|
||||
else:
|
||||
if node_version not in [v["node_version"] for v in VERSIONS]:
|
||||
node_version = VERSIONS[0]["node_version"]
|
||||
|
||||
if node_version == VERSIONS[0]["node_version"]:
|
||||
lib_path = LATEST_NODE_LIB_PATH
|
||||
else:
|
||||
lib_path = get_node_lib_path(node_version)
|
||||
|
||||
version_str = "v"+".".join([str(i) for i in list(node_version)])
|
||||
|
||||
lib_dir = bpy.path.abspath(self.lib_dir)
|
||||
local_lib_path: Path = Path(lib_dir,
|
||||
f"BioxelNodes_{version_str}.blend").resolve()
|
||||
node_lib_path: Path = lib_path
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
|
||||
if local_lib_path != node_lib_path:
|
||||
shutil.copy(node_lib_path, local_lib_path)
|
||||
|
||||
libs = []
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
if node_group.library:
|
||||
libs.append(node_group.library)
|
||||
|
||||
libs = list(set(libs))
|
||||
for lib in libs:
|
||||
lib.filepath = bpy.path.relpath(str(local_lib_path),
|
||||
start=str(blend_path))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bpy.data.is_saved
|
||||
from ..constants import LATEST_NODE_LIB_PATH
|
||||
from ..utils import (
|
||||
get_all_layer_objs,
|
||||
get_cache_dir,
|
||||
get_node_version,
|
||||
is_missing_layer,
|
||||
)
|
||||
|
||||
|
||||
class CleanTemp(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.clear_temp"
|
||||
bl_idname = "bioxel.clear_temp"
|
||||
bl_label = "Clean Temp"
|
||||
bl_description = "Clean all cache in temp (include other project cache)"
|
||||
|
||||
@@ -113,24 +24,26 @@ class CleanTemp(bpy.types.Operator):
|
||||
try:
|
||||
shutil.rmtree(cache_dir)
|
||||
self.report({"INFO"}, f"Successfully cleaned temp.")
|
||||
return {'FINISHED'}
|
||||
return {"FINISHED"}
|
||||
except:
|
||||
self.report({"WARNING"},
|
||||
"Fail to clean temp, you may do it manually.")
|
||||
return {'CANCELLED'}
|
||||
self.report(
|
||||
{"WARNING"}, "Fail to clean temp, you may do it manually.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message="All temp files will be removed, include other project cache, do you still want to clean?")
|
||||
return {'RUNNING_MODAL'}
|
||||
context.window_manager.invoke_confirm(
|
||||
self,
|
||||
event,
|
||||
message="All temp files will be removed, include other project cache, do you still want to clean?",
|
||||
)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class RenderSettingPreset(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.render_setting_preset"
|
||||
bl_idname = "bioxel.render_setting_preset"
|
||||
bl_label = "Render Setting Presets"
|
||||
bl_description = "Render setting presets for bioxel"
|
||||
bl_options = {'UNDO'}
|
||||
bl_options = {"UNDO"}
|
||||
|
||||
PRESETS = {
|
||||
"performance": "Performance",
|
||||
@@ -138,166 +51,201 @@ class RenderSettingPreset(bpy.types.Operator):
|
||||
"quality": "Quality",
|
||||
}
|
||||
|
||||
preset: bpy.props.EnumProperty(name="Preset",
|
||||
default="balance",
|
||||
items=[(k, v, "")
|
||||
for k, v in PRESETS.items()]) # type: ignore
|
||||
preset: bpy.props.EnumProperty(
|
||||
name="Preset", default="balance", items=[(k, v, "") for k, v in PRESETS.items()]
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if self.preset == "performance":
|
||||
# EEVEE
|
||||
# bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
# bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = "4"
|
||||
bpy.context.scene.eevee.volumetric_shadow_samples = 32
|
||||
bpy.context.scene.eevee.volumetric_samples = 64
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 1
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||
|
||||
# Cycles
|
||||
bpy.context.scene.cycles.volume_bounces = 0
|
||||
bpy.context.scene.cycles.volume_bounces = 4
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 4
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 4
|
||||
bpy.context.scene.cycles.volume_step_rate = 4
|
||||
bpy.context.scene.cycles.volume_biased = True
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 100
|
||||
bpy.context.scene.cycles.volume_step_rate = 100
|
||||
|
||||
elif self.preset == "balance":
|
||||
# EEVEE
|
||||
# bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
# bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = "2"
|
||||
bpy.context.scene.eevee.volumetric_shadow_samples = 64
|
||||
bpy.context.scene.eevee.volumetric_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 8
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||
|
||||
# Cycles
|
||||
bpy.context.scene.cycles.volume_bounces = 4
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 8
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 1
|
||||
bpy.context.scene.cycles.volume_step_rate = 1
|
||||
bpy.context.scene.cycles.volume_bounces = 8
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||
bpy.context.scene.cycles.volume_biased = True
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 10
|
||||
bpy.context.scene.cycles.volume_step_rate = 10
|
||||
|
||||
elif self.preset == "quality":
|
||||
# EEVEE
|
||||
# bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
# bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = "1"
|
||||
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_samples = 256
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 16
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||
|
||||
# Cycles
|
||||
bpy.context.scene.cycles.volume_bounces = 8
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 0.5
|
||||
bpy.context.scene.cycles.volume_step_rate = 0.5
|
||||
bpy.context.scene.cycles.volume_bounces = 16
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 32
|
||||
bpy.context.scene.cycles.volume_biased = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SliceViewer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.slice_viewer"
|
||||
bl_label = "Slice Viewer"
|
||||
bl_description = "A preview scene setting for viewing slicers"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
def execute(self, context):
|
||||
# bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 1
|
||||
|
||||
bpy.context.scene.eevee.use_shadows = False
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = False
|
||||
|
||||
view_3d = None
|
||||
if context.area.type == 'VIEW_3D':
|
||||
view_3d = context.area
|
||||
else:
|
||||
for area in context.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
view_3d = area
|
||||
break
|
||||
if view_3d:
|
||||
view_3d.spaces[0].shading.type = 'MATERIAL'
|
||||
view_3d.spaces[0].shading.studio_light = 'studio.exr'
|
||||
view_3d.spaces[0].shading.studiolight_intensity = 1.5
|
||||
view_3d.spaces[0].shading.use_scene_lights = False
|
||||
view_3d.spaces[0].shading.use_scene_world = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_all_layers(layer_filter=None):
|
||||
def _layer_filter(layer_obj):
|
||||
return True
|
||||
|
||||
layer_filter = layer_filter or _layer_filter
|
||||
layer_objs = get_all_layer_objs()
|
||||
return [obj for obj in layer_objs if layer_filter(obj)]
|
||||
|
||||
|
||||
class SaveAllLayersCache(bpy.types.Operator, SaveLayersCache):
|
||||
bl_idname = "bioxelnodes.save_all_layers_cache"
|
||||
bl_label = "Save All Layers Cache"
|
||||
bl_description = "Save all cache of this file"
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
success_msg = "Successfully saved all layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_not_missing(layer_obj):
|
||||
return not is_missing_layer(layer_obj)
|
||||
return get_all_layers(is_not_missing)
|
||||
|
||||
|
||||
class RemoveAllMissingLayers(bpy.types.Operator, RemoveLayers):
|
||||
bl_idname = "bioxelnodes.remove_all_missing_layers"
|
||||
bl_label = "Remove All Missing Layers"
|
||||
bl_description = "Remove all current container missing layers"
|
||||
bl_icon = "BRUSH_DATA"
|
||||
|
||||
success_msg = "Successfully removed all missing layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_missing(layer_obj):
|
||||
return is_missing_layer(layer_obj)
|
||||
return get_all_layers(is_missing)
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message=f"Are you sure to remove all **Missing** layers?")
|
||||
return {'RUNNING_MODAL'}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class Help(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.help"
|
||||
bl_idname = "bioxel.help"
|
||||
bl_label = "Need Help?"
|
||||
bl_description = "Online Manual for Beginner"
|
||||
bl_icon = "HELP"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open(
|
||||
'https://omoolab.github.io/BioxelNodes/latest/', new=2)
|
||||
webbrowser.open("https://omoolab.github.io/BioxelNodes/latest/", new=2)
|
||||
|
||||
return {'FINISHED'}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddEeveeEnv(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_eevee_env"
|
||||
bl_label = "Add EEVEE Env Light"
|
||||
bl_description = "To make volume shadow less dark"
|
||||
# 定义虚无化切换操作
|
||||
class TogglePhantom(bpy.types.Operator):
|
||||
"""Toggle object phantom state (ray invisible, no shadows, semi-transparent wireframe)"""
|
||||
|
||||
bl_idname = "bioxel.toggle_phantom"
|
||||
bl_label = "Toggle Phantom"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
bpy.ops.wm.append(
|
||||
directory=f"{str(LATEST_NODE_LIB_PATH)}/Object",
|
||||
filename="EeveeEnv"
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
self.report({"WARNING"}, "No object selected")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# 获取3D视图空间
|
||||
view_space = None
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
for space in area.spaces:
|
||||
if space.type == "VIEW_3D":
|
||||
view_space = space
|
||||
break
|
||||
if view_space:
|
||||
break
|
||||
|
||||
# 确保存储状态的文本数据块存在
|
||||
text_name = "phantom_orig_states"
|
||||
if text_name not in bpy.data.texts:
|
||||
bpy.data.texts.new(text_name)
|
||||
|
||||
# 从文本数据块加载状态
|
||||
states_text = bpy.data.texts[text_name]
|
||||
try:
|
||||
states = json.loads(states_text.as_string())
|
||||
except:
|
||||
states = {}
|
||||
|
||||
def get_obj_uuid(obj):
|
||||
if "bioxel_uuid" not in obj:
|
||||
obj["bioxel_uuid"] = uuid.uuid4().hex
|
||||
return obj["bioxel_uuid"]
|
||||
|
||||
# 使用物体的唯一标识符来追踪
|
||||
obj_uuid = get_obj_uuid(obj)
|
||||
|
||||
# 检查物体是否已经处于虚无化状态
|
||||
is_phantom = (
|
||||
obj.display_type == "SOLID"
|
||||
and obj.show_wire
|
||||
and obj.show_in_front
|
||||
and round(obj.color[3], 2) == 0.10
|
||||
and not obj.visible_camera
|
||||
and not obj.visible_diffuse
|
||||
and not obj.visible_glossy
|
||||
and not obj.visible_transmission
|
||||
and not obj.visible_volume_scatter
|
||||
and not obj.visible_shadow
|
||||
)
|
||||
|
||||
bpy.context.scene.eevee.use_shadows = True
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||
if is_phantom:
|
||||
# 恢复原始状态
|
||||
if obj_uuid in states:
|
||||
state = states[obj_uuid]
|
||||
obj.display_type = state["display_type"]
|
||||
obj.show_wire = state["show_wire"]
|
||||
obj.show_in_front = state["show_in_front"]
|
||||
obj.color = state["color"]
|
||||
obj.visible_camera = state["visible_camera"]
|
||||
obj.visible_diffuse = state["visible_diffuse"]
|
||||
obj.visible_glossy = state["visible_glossy"]
|
||||
obj.visible_transmission = state["visible_transmission"]
|
||||
obj.visible_volume_scatter = state["visible_volume_scatter"]
|
||||
obj.visible_shadow = state["visible_shadow"]
|
||||
|
||||
return {'FINISHED'}
|
||||
# 恢复视图着色设置(如果之前有保存)
|
||||
if view_space and "viewport_color_type" in state:
|
||||
view_space.shading.color_type = state["viewport_color_type"]
|
||||
|
||||
del states[obj_uuid]
|
||||
self.report({"INFO"}, f"Object restored")
|
||||
else:
|
||||
# 保存原始状态
|
||||
state_data = {
|
||||
"display_type": obj.display_type,
|
||||
"show_wire": obj.show_wire,
|
||||
"show_in_front": obj.show_in_front,
|
||||
"color": list(obj.color), # 保存RGBA颜色值
|
||||
"visible_camera": obj.visible_camera,
|
||||
"visible_diffuse": obj.visible_diffuse,
|
||||
"visible_glossy": obj.visible_glossy,
|
||||
"visible_transmission": obj.visible_transmission,
|
||||
"visible_volume_scatter": obj.visible_volume_scatter,
|
||||
"visible_shadow": obj.visible_shadow,
|
||||
}
|
||||
|
||||
# 检查并处理视图着色设置
|
||||
if view_space:
|
||||
# 保存当前视图着色设置
|
||||
state_data["viewport_color_type"] = view_space.shading.color_type
|
||||
|
||||
# 检查当前是否为object, random或attribute
|
||||
valid_types = {"OBJECT", "RANDOM", "ATTRIBUTE"}
|
||||
if view_space.shading.color_type not in valid_types:
|
||||
# 改为object模式
|
||||
view_space.shading.color_type = "OBJECT"
|
||||
|
||||
states[obj_uuid] = state_data
|
||||
|
||||
# 设置虚无化状态
|
||||
obj.display_type = "SOLID" # 以实体方式显示
|
||||
obj.show_wire = True # 显示线框
|
||||
obj.show_in_front = True # 显示在其他物体前面
|
||||
# 保持RGB不变,仅修改alpha透明度为0.1
|
||||
obj.color[3] = 0.1
|
||||
# 关闭所有射线可见性和投影
|
||||
obj.visible_camera = False
|
||||
obj.visible_diffuse = False
|
||||
obj.visible_glossy = False
|
||||
obj.visible_transmission = False
|
||||
obj.visible_volume_scatter = False
|
||||
obj.visible_shadow = False
|
||||
self.report({"INFO"}, f"Object phantomed")
|
||||
|
||||
# 保存状态到文本数据块
|
||||
states_text.clear()
|
||||
states_text.write(json.dumps(states, indent=2))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
Reference in New Issue
Block a user