2025-12-01
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
reloadable_modules = [
|
||||
'color_utils',
|
||||
'export_utils',
|
||||
'ui_utils',
|
||||
'preset_utils',
|
||||
'installation_utils',
|
||||
'version_compatibility_utils',
|
||||
'cache_utils',
|
||||
'api_workaround_utils',
|
||||
]
|
||||
for module_name in reloadable_modules:
|
||||
if module_name in locals():
|
||||
importlib.reload(locals()[module_name])
|
||||
|
||||
import bpy
|
||||
|
||||
from . import (
|
||||
color_utils,
|
||||
export_utils,
|
||||
ui_utils,
|
||||
preset_utils,
|
||||
installation_utils,
|
||||
version_compatibility_utils,
|
||||
cache_utils,
|
||||
api_workaround_utils,
|
||||
)
|
||||
@@ -0,0 +1,405 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from . import version_compatibility_utils as vcu
|
||||
from .. import render
|
||||
|
||||
|
||||
# Workaround for https://developer.blender.org/T71908
|
||||
# This bug can cause keyframed parameters not to be evaluated during rendering
|
||||
# when a frame_change handler is used.
|
||||
#
|
||||
# This workaround works by forcing an object to be evaluated and then setting
|
||||
# the original object value to the evaluated values. This workaround can only
|
||||
# be applied to Blender versions 2.81 and later.
|
||||
def frame_change_post_apply_T71908_workaround(context, depsgraph=None):
|
||||
if not render.is_rendering():
|
||||
return
|
||||
if not vcu.is_blender_281():
|
||||
return
|
||||
|
||||
dprops = context.scene.flip_fluid.get_domain_properties()
|
||||
if dprops is None:
|
||||
return
|
||||
|
||||
# Apply to Domain render properties
|
||||
|
||||
domain_object = context.scene.flip_fluid.get_domain_object()
|
||||
if depsgraph is None:
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
|
||||
domain_object_eval = domain_object.evaluated_get(depsgraph)
|
||||
dprops_eval = domain_object_eval.flip_fluid.domain
|
||||
|
||||
property_paths = dprops.property_registry.get_property_paths()
|
||||
render_paths = [p.split('.')[-1] for p in property_paths if p.startswith("domain.render")]
|
||||
for p in render_paths:
|
||||
setattr(dprops.render, p, getattr(dprops_eval.render, p))
|
||||
|
||||
# Apply to FLIP Fluids sidebar
|
||||
dprops.render.override_frame = dprops_eval.render.override_frame
|
||||
|
||||
# Apply to any Ocean Modifer's 'Time' value on the mesh objects, a common issue for this bug
|
||||
|
||||
cache_objects = [
|
||||
dprops.mesh_cache.surface.get_cache_object(),
|
||||
dprops.mesh_cache.particles.get_cache_object(),
|
||||
dprops.mesh_cache.foam.get_cache_object(),
|
||||
dprops.mesh_cache.bubble.get_cache_object(),
|
||||
dprops.mesh_cache.spray.get_cache_object(),
|
||||
dprops.mesh_cache.dust.get_cache_object()
|
||||
]
|
||||
cache_objects = [x for x in cache_objects if x]
|
||||
|
||||
for obj in cache_objects:
|
||||
obj_eval = obj.evaluated_get(depsgraph)
|
||||
for i in range(len(obj.modifiers)):
|
||||
if obj.modifiers[i].type == 'OCEAN':
|
||||
obj.modifiers[i].time = obj_eval.modifiers[i].time
|
||||
|
||||
# Apply to any FF_GeometryNodes geometry node 'Motion Blur Scale' value on the mesh objects, another issue
|
||||
# for this bug when adjusting motion blur for slow motion simulations.
|
||||
# Also apply to other FF_GeometryNodes inputs in case the user wants to keyframe these values.
|
||||
|
||||
input_name_list_surface = [
|
||||
"Input_4", # Motion Blur Scale
|
||||
"Input_6", # Enable Motion Blur
|
||||
"Socket_0", # Blur Velocity For Fading
|
||||
"Socket_5", # Shade Smooth Surface
|
||||
"Socket_6", # Blur Iterations
|
||||
]
|
||||
|
||||
input_name_list_particles = [
|
||||
"Input_4", # Motion Blur Scale
|
||||
"Input_6", # Particle Scale
|
||||
"Input_8", # Enable Motion Blur
|
||||
"Input_9", # Enable Point Cloud
|
||||
"Input_10", # Enable Instancing
|
||||
"Socket_0", # Fading Strength
|
||||
"Socket_1", # Fading Width
|
||||
"Socket_2", # Particle Scale Random
|
||||
"Socket_4", # Fading Density
|
||||
"Socket_9", # Shade Smooth Instancing
|
||||
]
|
||||
|
||||
for obj in cache_objects:
|
||||
obj_eval = obj.evaluated_get(depsgraph)
|
||||
for i in range(len(obj.modifiers)):
|
||||
if obj.modifiers[i].type == 'NODES' and obj.modifiers[i].name.startswith("FF_GeometryNodes"):
|
||||
mod_name = obj.modifiers[i].name
|
||||
if mod_name.startswith("FF_GeometryNodesSurface"):
|
||||
input_name_list = input_name_list_surface
|
||||
elif mod_name.startswith("FF_GeometryNodesFluidParticles") or mod_name.startswith("FF_GeometryNodesWhitewater"):
|
||||
input_name_list = input_name_list_particles
|
||||
else:
|
||||
continue
|
||||
|
||||
for input_name in input_name_list:
|
||||
if input_name in obj.modifiers[i]:
|
||||
obj.modifiers[i][input_name] = obj_eval.modifiers[i][input_name]
|
||||
|
||||
|
||||
# In some versions of Blender the viewport rendered view is
|
||||
# not updated to display and object if the object's 'hide_render'
|
||||
# property has changed or ray visibility has changed via Python.
|
||||
# Toggling the object's hide_viewport option on and off
|
||||
# is a workaround to get the viewport to update.
|
||||
#
|
||||
# Note: toggling hide_viewport will deselect the object, so this workaround
|
||||
# will also re-select the object if needed.
|
||||
def toggle_viewport_visibility_to_update_rendered_viewport_workaround(bl_object):
|
||||
is_selected = vcu.select_get(bl_object)
|
||||
vcu.toggle_outline_eye_icon(bl_object)
|
||||
vcu.toggle_outline_eye_icon(bl_object)
|
||||
if is_selected:
|
||||
vcu.select_set(bl_object, True)
|
||||
|
||||
|
||||
# Due to API changes in Cycles visibility properties in Blender 3.0, this will
|
||||
# break compatibility when opening a .blend file saved in Blender 3.0 in earlier
|
||||
# versions of Blender. This method updates FLIP Fluid object cycles visibility
|
||||
# settings for
|
||||
def load_post_update_cycles_visibility_forward_compatibility_from_blender_3():
|
||||
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
|
||||
if dprops is None:
|
||||
return
|
||||
|
||||
last_version = dprops.debug.get_last_saved_blender_version()
|
||||
current_version = bpy.app.version
|
||||
|
||||
if last_version == (-1, -1, -1):
|
||||
# Skip, file contains no version history.
|
||||
return
|
||||
|
||||
if current_version >= last_version:
|
||||
# No compatibility update needed
|
||||
return
|
||||
|
||||
# Downgrading from Blender 3.x. Compatibility update needed.
|
||||
def set_cycles_ray_visibility(bl_object, is_enabled):
|
||||
# Cycles may not be enabled in the user's preferences
|
||||
try:
|
||||
if vcu.is_blender_30():
|
||||
bl_object.visible_camera = is_enabled
|
||||
bl_object.visible_diffuse = is_enabled
|
||||
bl_object.visible_glossy = is_enabled
|
||||
bl_object.visible_transmission = is_enabled
|
||||
bl_object.visible_volume_scatter = is_enabled
|
||||
bl_object.visible_shadow = is_enabled
|
||||
else:
|
||||
bl_object.cycles_visibility.camera = is_enabled
|
||||
bl_object.cycles_visibility.transmission = is_enabled
|
||||
bl_object.cycles_visibility.diffuse = is_enabled
|
||||
bl_object.cycles_visibility.scatter = is_enabled
|
||||
bl_object.cycles_visibility.glossy = is_enabled
|
||||
bl_object.cycles_visibility.shadow = is_enabled
|
||||
except:
|
||||
pass
|
||||
|
||||
flip_props = bpy.context.scene.flip_fluid
|
||||
invisible_objects = ([flip_props.get_domain_object()] +
|
||||
flip_props.get_fluid_objects() +
|
||||
flip_props.get_inflow_objects() +
|
||||
flip_props.get_outflow_objects() +
|
||||
flip_props.get_force_field_objects())
|
||||
|
||||
for bl_object in invisible_objects:
|
||||
set_cycles_ray_visibility(bl_object, False)
|
||||
toggle_viewport_visibility_to_update_rendered_viewport_workaround(bl_object)
|
||||
|
||||
|
||||
def get_enabled_features_affected_by_T88811(domain_properties=None):
|
||||
if domain_properties is None:
|
||||
domain_properties = bpy.context.scene.flip_fluid.get_domain_properties()
|
||||
if domain_properties is None:
|
||||
return None
|
||||
dprops = domain_properties
|
||||
|
||||
data_dict = {}
|
||||
data_dict["attributes"] = {}
|
||||
data_dict["attributes"]["surface"] = []
|
||||
data_dict["attributes"]["whitewater"] = []
|
||||
data_dict["fluidparticles"] = []
|
||||
data_dict["viscosity"] = []
|
||||
|
||||
if dprops.surface.enable_velocity_vector_attribute:
|
||||
data_dict["attributes"]["surface"].append("Velocity")
|
||||
if dprops.surface.enable_speed_attribute:
|
||||
data_dict["attributes"]["surface"].append("Speed")
|
||||
if dprops.surface.enable_vorticity_vector_attribute:
|
||||
data_dict["attributes"]["surface"].append("Vorticity")
|
||||
if dprops.surface.enable_color_attribute:
|
||||
data_dict["attributes"]["surface"].append("Color")
|
||||
if dprops.surface.enable_age_attribute:
|
||||
data_dict["attributes"]["surface"].append("Age")
|
||||
if dprops.surface.enable_lifetime_attribute:
|
||||
data_dict["attributes"]["surface"].append("Lifetime")
|
||||
if dprops.surface.enable_whitewater_proximity_attribute:
|
||||
data_dict["attributes"]["surface"].append("Whitewater Proximity")
|
||||
if dprops.surface.enable_source_id_attribute:
|
||||
data_dict["attributes"]["surface"].append("Source ID")
|
||||
|
||||
if dprops.whitewater.enable_velocity_vector_attribute:
|
||||
data_dict["attributes"]["whitewater"].append("Velocity")
|
||||
|
||||
# Disabled to prevent the warning popup from displaying on default settings (Whitewater ID enabled by default)
|
||||
# TODO: Rework this warning to be less intrusive
|
||||
"""
|
||||
if dprops.whitewater.enable_id_attribute:
|
||||
data_dict["attributes"]["whitewater"].append("ID")
|
||||
"""
|
||||
|
||||
if dprops.whitewater.enable_lifetime_attribute:
|
||||
data_dict["attributes"]["whitewater"].append("Lifetime")
|
||||
|
||||
if dprops.particles.enable_fluid_particle_output:
|
||||
data_dict["fluidparticles"].append("Fluid particle export and particle attributes")
|
||||
|
||||
if dprops.world.enable_viscosity and dprops.surface.enable_viscosity_attribute:
|
||||
data_dict["viscosity"].append("Variable Viscosity")
|
||||
|
||||
contains_info = (
|
||||
data_dict["attributes"]["surface"] or
|
||||
data_dict["attributes"]["whitewater"] or
|
||||
data_dict["fluidparticles"] or
|
||||
data_dict["viscosity"]
|
||||
)
|
||||
if not contains_info:
|
||||
return None
|
||||
|
||||
return data_dict
|
||||
|
||||
|
||||
def get_enabled_features_string_T88811(feature_list):
|
||||
features_str = ""
|
||||
for i, item in enumerate(feature_list):
|
||||
features_str += item
|
||||
if i != len(feature_list) - 1:
|
||||
features_str += ", "
|
||||
return features_str
|
||||
|
||||
|
||||
def draw_T88811_ui_warning(ui_box, preferences, feature_dict):
|
||||
row = ui_box.row(align=True)
|
||||
row.alert = True
|
||||
row.label(text="FLIP Fluids: Possible Render Crash Warning:", icon="ERROR")
|
||||
column = ui_box.column(align=True)
|
||||
column.label(text="A current bug in Blender may cause frequent")
|
||||
column.label(text="render crashes or incorrect renders when")
|
||||
column.label(text="using the following enabled features:")
|
||||
column.separator()
|
||||
|
||||
if feature_dict["attributes"]["surface"]:
|
||||
column.label(text="Surface Attributes:", icon="ERROR")
|
||||
column.label(text=get_enabled_features_string_T88811(feature_dict["attributes"]["surface"]), icon="DOT")
|
||||
if feature_dict["attributes"]["whitewater"]:
|
||||
column.label(text="Whitewater Attributes:", icon="ERROR")
|
||||
column.label(text=get_enabled_features_string_T88811(feature_dict["attributes"]["whitewater"]), icon="DOT")
|
||||
if feature_dict["fluidparticles"]:
|
||||
column.label(text="Fluid Particle Features:", icon="ERROR")
|
||||
column.label(text=get_enabled_features_string_T88811(feature_dict["fluidparticles"]), icon="DOT")
|
||||
if feature_dict["viscosity"]:
|
||||
column.label(text="Viscosity Features:", icon="ERROR")
|
||||
column.label(text=get_enabled_features_string_T88811(feature_dict["viscosity"]), icon="DOT")
|
||||
column.separator()
|
||||
|
||||
column.label(text="This bug can be prevented by rendering", icon="INFO")
|
||||
column.label(text="from the command line. See the cmd")
|
||||
column.label(text="rendering tools in the FLIP Fluids sidebar.")
|
||||
|
||||
column.operator(
|
||||
"wm.url_open",
|
||||
text="Documentation: Command Line Tools",
|
||||
icon="URL"
|
||||
).url = "https://github.com/rlguy/Blender-FLIP-Fluids/wiki/Helper-Menu-Settings#command-line-tools"
|
||||
column.operator(
|
||||
"wm.url_open",
|
||||
text="Bug Report: T88811",
|
||||
icon="URL"
|
||||
).url = "https://projects.blender.org/blender/blender/issues/88811"
|
||||
|
||||
column.prop(preferences, "dismiss_T88811_crash_warning", text="Do not show this warning again")
|
||||
|
||||
def get_T88811_cmd_warning_string(feature_dict):
|
||||
warning = ""
|
||||
warning += "************************************************\n"
|
||||
warning += "FLIP Fluids: Possible Render Crash Warning\n\n"
|
||||
warning += "A current bug in Blender may cause frequent render crashes or incorrect renders when using the following enabled features:\n\n"
|
||||
|
||||
if feature_dict["attributes"]["surface"]:
|
||||
warning += "* Surface Attributes:\n"
|
||||
warning += " - " + get_enabled_features_string_T88811(feature_dict["attributes"]["surface"]) + "\n"
|
||||
if feature_dict["attributes"]["whitewater"]:
|
||||
warning += "* Whitewater Attributes:\n"
|
||||
warning += " - " + get_enabled_features_string_T88811(feature_dict["attributes"]["whitewater"]) + "\n"
|
||||
if feature_dict["viscosity"]:
|
||||
warning += "* Viscosity Features:\n"
|
||||
warning += " - " + get_enabled_features_string_T88811(feature_dict["viscosity"]) + "\n"
|
||||
|
||||
warning += "\n"
|
||||
warning += "This bug can be prevented by rendering from the command line.\n"
|
||||
warning += "See the command line rendering tools in the FLIP Fluids sidebar.\n\n"
|
||||
warning += "Command Line Tools Documentation:\n https://github.com/rlguy/Blender-FLIP-Fluids/wiki/Helper-Menu-Settings#command-line-tools\n"
|
||||
warning += "Bug Report T88811:\n https://developer.blender.org/T88811\n"
|
||||
warning += "************************************************\n"
|
||||
|
||||
return warning
|
||||
|
||||
|
||||
def is_persistent_data_issue_relevant():
|
||||
if bpy.context.scene.render.engine != 'CYCLES':
|
||||
return False
|
||||
domain_properties = bpy.context.scene.flip_fluid.get_domain_properties()
|
||||
if domain_properties is None:
|
||||
return False
|
||||
return bpy.context.scene.render.use_persistent_data
|
||||
|
||||
|
||||
def draw_persistent_data_warning(ui_box, preferences):
|
||||
row = ui_box.row(align=True)
|
||||
row.alert = True
|
||||
row.label(text="FLIP Fluids: Incompatible Render Option Warning:", icon="ERROR")
|
||||
column = ui_box.column(align=True)
|
||||
column.label(text="The Cycles 'Persistent Data' render option is not")
|
||||
column.label(text="compatible with the simulation meshes. This may")
|
||||
column.label(text="cause static renders, incorrect renders, or")
|
||||
column.label(text="render crashes.")
|
||||
column.separator()
|
||||
column.label(text="This issue can be prevented by disabling")
|
||||
column.label(text="Render Properties > Performance > Persistent Data:")
|
||||
|
||||
row = column.row(align=True)
|
||||
row.alignment = 'LEFT'
|
||||
row.label(text=" ")
|
||||
row.prop(bpy.context.scene.render, "use_persistent_data")
|
||||
|
||||
column.label(text="Or by rendering from the command line. See the")
|
||||
column.label(text="cmd rendering tools in the FLIP Fluids sidebar.")
|
||||
|
||||
column.operator(
|
||||
"wm.url_open",
|
||||
text="Documentation: Command Line Tools",
|
||||
icon="URL"
|
||||
).url = "https://github.com/rlguy/Blender-FLIP-Fluids/wiki/Helper-Menu-Settings#command-line-tools"
|
||||
|
||||
column.prop(preferences, "dismiss_persistent_data_render_warning", text="Do not show this warning again")
|
||||
|
||||
|
||||
def get_persistent_data_warning_string():
|
||||
warning = ""
|
||||
warning += "************************************************\n"
|
||||
warning += "FLIP Fluids: Incompatible Render Option Warning\n\n"
|
||||
warning += "The Cycles 'Persistent Data' render option is not compatible with the simulation meshes. This may cause static renders, incorrect renders, or render crashes.\n\n"
|
||||
warning += "This issue can be prevented by disabling the 'Render Properties > Performance > Persistent Data' option or by rendering from the command line.\n"
|
||||
warning += "See the command line rendering tools in the FLIP Fluids sidebar.\n\n"
|
||||
warning += "Command Line Tools Documentation:\n https://github.com/rlguy/Blender-FLIP-Fluids/wiki/Helper-Menu-Settings#command-line-tools\n"
|
||||
warning += "************************************************\n"
|
||||
return warning
|
||||
|
||||
|
||||
# Blender will crash during render if:
|
||||
# (Cycles render is used and motion blur is enabled) or other renderers are used
|
||||
# and if there is an object with a keyframed hide_render property
|
||||
#
|
||||
# In rare cases, Blender may also crash regardless of the above condition if there is an object with a
|
||||
# keyframed hide_render property. It is not certain what exact conditions are required for this case.
|
||||
#
|
||||
# Issue thread: https://github.com/rlguy/Blender-FLIP-Fluids/issues/566
|
||||
#
|
||||
# Workaround: detect these cases and remove depsgraph.update() calls during render calls
|
||||
# which will prevent the crash. Note: depsgraph.update() in our use case is not
|
||||
# supported in the Python API but has a side effect of making the render more stable.
|
||||
# Removing these calls will make the render more likely to crash, so rendering from the
|
||||
# command line is recommended in these cases.
|
||||
def is_keyframed_hide_render_issue_relevant(scene):
|
||||
is_relevant = False
|
||||
using_cycles = scene.render.engine == 'CYCLES'
|
||||
override_condition = True
|
||||
if (using_cycles and scene.render.use_motion_blur) or not using_cycles or override_condition:
|
||||
for obj in bpy.data.objects:
|
||||
if not obj.animation_data:
|
||||
continue
|
||||
anim_data = obj.animation_data
|
||||
if not anim_data.action or not anim_data.action.fcurves:
|
||||
continue
|
||||
|
||||
for fcurve in anim_data.action.fcurves:
|
||||
if fcurve.data_path == "hide_render":
|
||||
is_relevant = True
|
||||
break
|
||||
|
||||
return is_relevant
|
||||
@@ -0,0 +1,46 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy, os, json, time
|
||||
|
||||
from . import version_compatibility_utils as vcu
|
||||
|
||||
|
||||
def get_sounds_directory():
|
||||
return os.path.join(vcu.get_addon_directory(), "resources", "sounds")
|
||||
|
||||
|
||||
def play_sound(json_audio_filepath, block=False):
|
||||
if not vcu.is_blender_28():
|
||||
# aud not supported in Blender 2.79 or lower
|
||||
return
|
||||
|
||||
import aud
|
||||
|
||||
with open(json_audio_filepath, 'r', encoding='utf-8') as f:
|
||||
json_data = json.loads(f.read())
|
||||
|
||||
audio_length = float(json_data["length"])
|
||||
audio_filename = json_data["filename"]
|
||||
audio_filepath = os.path.join(os.path.dirname(json_audio_filepath), audio_filename)
|
||||
|
||||
device = aud.Device()
|
||||
sound = aud.Sound(audio_filepath)
|
||||
handle = device.play(sound)
|
||||
|
||||
if block:
|
||||
time.sleep(audio_length)
|
||||
handle.stop()
|
||||
@@ -0,0 +1,34 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hashlib
|
||||
|
||||
|
||||
def string_to_cache_slug(string):
|
||||
max_hash_len = 16
|
||||
max_string_len = 64 - max_hash_len
|
||||
whitelist = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
|
||||
|
||||
h = hashlib.md5()
|
||||
h.update(bytes(string, 'utf-8'))
|
||||
hexstr = h.hexdigest()
|
||||
hexstr = hexstr[:max_hash_len]
|
||||
|
||||
slug = string[:max_string_len]
|
||||
slug = ''.join(c if c in whitelist else '-' for c in slug)
|
||||
slug += hexstr
|
||||
|
||||
return slug
|
||||
@@ -0,0 +1,36 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
from ..ffengine import (
|
||||
mixbox,
|
||||
)
|
||||
|
||||
|
||||
def mixbox_lerp_srgb32f(r1, g1, b1, r2, g2, b2, t):
|
||||
if not mixbox.is_initialized():
|
||||
__initialize_mixbox()
|
||||
return mixbox.lerp_srgb32f(r1, g1, b1, r2, g2, b2, t)
|
||||
|
||||
|
||||
def __initialize_mixbox():
|
||||
module_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
lut_filepath = os.path.join(module_dir, "third_party", "mixbox", "mixbox_lut_data.bin")
|
||||
with open(lut_filepath, 'rb') as f:
|
||||
lut_data = f.read()
|
||||
lut_data_bytes = len(lut_data)
|
||||
mixbox.initialize(lut_data, lut_data_bytes)
|
||||
@@ -0,0 +1,482 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector, Matrix, Quaternion, Euler, Color
|
||||
from . import version_compatibility_utils as vcu
|
||||
|
||||
|
||||
def flip_fluid_object_to_dict(obj, object_properties):
|
||||
object_properties.refresh_property_registry()
|
||||
|
||||
d = {}
|
||||
for p in object_properties.property_registry.properties:
|
||||
path_elements = p.path.split('.')
|
||||
identifier = path_elements[-1]
|
||||
path_elements.pop(0)
|
||||
path_elements.pop()
|
||||
|
||||
group = object_properties
|
||||
dict_group = d
|
||||
for subgroup in path_elements:
|
||||
if subgroup not in dict_group.keys():
|
||||
dict_group[subgroup] = {}
|
||||
group = getattr(group, subgroup)
|
||||
dict_group = dict_group[subgroup]
|
||||
|
||||
item = getattr(group, identifier)
|
||||
dict_group[identifier] = item
|
||||
|
||||
if (isinstance(item, Vector) or isinstance(item, Color) or
|
||||
(hasattr(item, "__iter__") and not isinstance(item, str))):
|
||||
dict_group[identifier] = get_vector_property_data_dict(obj, group, identifier, len(item))
|
||||
elif hasattr(item, "is_min_max_property"):
|
||||
dict_group[identifier] = get_min_max_property_data_dict(obj, group, identifier)
|
||||
else:
|
||||
full_path = "flip_fluid." + p.path
|
||||
dict_group[identifier] = get_property_data_dict_from_path(obj, group, full_path)
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def is_property_animated(obj, prop_name, index=0, use_exact_path=False):
|
||||
anim_data = obj.animation_data
|
||||
if not anim_data or not anim_data.action or not anim_data.action.fcurves:
|
||||
return False
|
||||
|
||||
for fcurve in anim_data.action.fcurves:
|
||||
path = fcurve.data_path
|
||||
is_match = path.endswith(prop_name)
|
||||
if use_exact_path:
|
||||
is_match = path == prop_name
|
||||
if is_match and fcurve.array_index == index:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_property_path_animated(obj, path_name, index = 0):
|
||||
anim_data = obj.animation_data
|
||||
if not anim_data or not anim_data.action or not anim_data.action.fcurves:
|
||||
return False
|
||||
|
||||
for fcurve in anim_data.action.fcurves:
|
||||
if fcurve.data_path == path_name and fcurve.array_index == index:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_vector_animated(obj, prop_name, vector_size = 3):
|
||||
is_animated = False
|
||||
for i in range(vector_size):
|
||||
is_animated = is_animated or is_property_animated(obj, prop_name, i)
|
||||
return is_animated
|
||||
|
||||
|
||||
def get_property_fcurve(obj, prop_name, index=0, use_exact_path=False):
|
||||
anim_data = obj.animation_data
|
||||
for fcurve in anim_data.action.fcurves:
|
||||
path = fcurve.data_path
|
||||
|
||||
is_match = path.endswith(prop_name)
|
||||
if use_exact_path:
|
||||
is_match = path == prop_name
|
||||
if is_match and fcurve.array_index == index:
|
||||
return fcurve
|
||||
|
||||
|
||||
def get_property_fcurve_from_path(obj, path_name, index = 0):
|
||||
anim_data = obj.animation_data
|
||||
for fcurve in anim_data.action.fcurves:
|
||||
if fcurve.data_path == path_name and fcurve.array_index == index:
|
||||
return fcurve
|
||||
|
||||
|
||||
def get_property_data_dict(obj, prop_group, prop_name, index=None, use_exact_path=False):
|
||||
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
|
||||
|
||||
is_index_set = index != None
|
||||
if not is_index_set:
|
||||
index = 0
|
||||
|
||||
if is_property_animated(obj, prop_name, index, use_exact_path=use_exact_path):
|
||||
values = []
|
||||
fcurve = get_property_fcurve(obj, prop_name, index, use_exact_path=use_exact_path)
|
||||
frame_start, frame_end = dprops.simulation.get_frame_range()
|
||||
for i in range(frame_start, frame_end + 1):
|
||||
values.append(fcurve.evaluate(i))
|
||||
|
||||
is_values_constant = True
|
||||
for i in range(len(values)):
|
||||
if values[i] != values[0]:
|
||||
is_values_constant = False
|
||||
break
|
||||
|
||||
if is_values_constant:
|
||||
return {'is_animated' : False, 'data' : values[0]}
|
||||
else:
|
||||
return {'is_animated' : True, 'data' : values}
|
||||
else:
|
||||
prop_name_suffix = prop_name.split(".")[-1]
|
||||
value = getattr(prop_group, prop_name_suffix)
|
||||
if is_index_set:
|
||||
value = value[index]
|
||||
return {'is_animated' : False, 'data' : value}
|
||||
|
||||
|
||||
def get_property_data_dict_from_path(obj, prop_group, path_name, index = None):
|
||||
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
|
||||
|
||||
is_index_set = index != None
|
||||
if not is_index_set:
|
||||
index = 0
|
||||
|
||||
if is_property_path_animated(obj, path_name, index):
|
||||
values = []
|
||||
fcurve = get_property_fcurve_from_path(obj, path_name, index)
|
||||
frame_start, frame_end = dprops.simulation.get_frame_range()
|
||||
for i in range(frame_start, frame_end + 1):
|
||||
values.append(fcurve.evaluate(i))
|
||||
|
||||
is_values_constant = True
|
||||
for i in range(len(values)):
|
||||
if values[i] != values[0]:
|
||||
is_values_constant = False
|
||||
break
|
||||
|
||||
if is_values_constant:
|
||||
return {'is_animated' : False, 'data' : values[0]}
|
||||
else:
|
||||
return {'is_animated' : True, 'data' : values}
|
||||
else:
|
||||
prop_name = path_name.split(".")[-1]
|
||||
value = getattr(prop_group, prop_name)
|
||||
if is_index_set:
|
||||
value = value[index]
|
||||
return {'is_animated' : False, 'data' : value}
|
||||
|
||||
|
||||
def get_vector_property_data_dict(obj, prop_group, prop_name, vector_size=3, use_exact_path=False):
|
||||
component_dicts = []
|
||||
for i in range(vector_size):
|
||||
component_dicts.append(get_property_data_dict(obj,
|
||||
prop_group,
|
||||
prop_name, i, use_exact_path=use_exact_path))
|
||||
is_animated = False
|
||||
for i in range(vector_size):
|
||||
is_animated = is_animated or component_dicts[i]['is_animated']
|
||||
|
||||
if is_animated:
|
||||
numvals = 0
|
||||
for i in range(vector_size):
|
||||
if component_dicts[i]['is_animated']:
|
||||
numvals = max(numvals, len(component_dicts[i]['data']))
|
||||
|
||||
for i in range(vector_size):
|
||||
if not component_dicts[i]['is_animated']:
|
||||
component_dicts[i]['data'] = [component_dicts[i]['data']] * numvals
|
||||
|
||||
values = []
|
||||
for i in range(numvals):
|
||||
vector = []
|
||||
for j in range(vector_size):
|
||||
vector.append(component_dicts[j]['data'][i])
|
||||
values.append(vector)
|
||||
|
||||
return {'is_animated' : True, 'data' : values}
|
||||
else:
|
||||
value = []
|
||||
for i in range(vector_size):
|
||||
value.append(component_dicts[i]['data'])
|
||||
|
||||
return {'is_animated' : False, 'data' : value}
|
||||
|
||||
|
||||
def get_min_max_property_data_dict(obj, prop_group, prop_name):
|
||||
prop_group = getattr(prop_group, prop_name)
|
||||
|
||||
# Multiple properties attached to a single object may have
|
||||
# 'value_min' or 'value_max' as the name. In order to correctly
|
||||
# search for the property, set the identifier as a path
|
||||
# Ex: prop_name.value_min
|
||||
identifier_min = prop_name + ".value_min"
|
||||
identifier_max = prop_name + ".value_max"
|
||||
min_dict = get_property_data_dict(obj, prop_group, identifier_min)
|
||||
max_dict = get_property_data_dict(obj, prop_group, identifier_max)
|
||||
|
||||
is_animated = min_dict['is_animated'] or max_dict['is_animated']
|
||||
if is_animated:
|
||||
numvals = 0
|
||||
if min_dict['is_animated']:
|
||||
numvals = max(numvals, len(min_dict['data']))
|
||||
if max_dict['is_animated']:
|
||||
numvals = max(numvals, len(max_dict['data']))
|
||||
|
||||
if not min_dict['is_animated']:
|
||||
min_dict['data'] = [min_dict['data']] * numvals
|
||||
if not max_dict['is_animated']:
|
||||
max_dict['data'] = [max_dict['data']] * numvals
|
||||
|
||||
values = []
|
||||
for i in range(numvals):
|
||||
values.append([min_dict['data'][i], max_dict['data'][i]])
|
||||
return {'is_animated' : True, 'data' : values}
|
||||
else:
|
||||
value = [min_dict['data'], max_dict['data']]
|
||||
return {'is_animated' : False, 'data' : value}
|
||||
|
||||
|
||||
def get_rotation_mode_data_dict(obj):
|
||||
data = get_property_data_dict(obj, obj, 'rotation_mode')
|
||||
if not data['is_animated']:
|
||||
return data
|
||||
|
||||
str_values = []
|
||||
for n in data['data']:
|
||||
strval = ''
|
||||
if n == -1:
|
||||
strval = 'AXIS_ANGLE'
|
||||
elif n == 0:
|
||||
strval = 'QUATERNION'
|
||||
elif n == 1:
|
||||
strval = 'XYZ'
|
||||
elif n == 2:
|
||||
strval = 'XZY'
|
||||
elif n == 3:
|
||||
strval = 'YXZ'
|
||||
elif n == 4:
|
||||
strval = 'YZX'
|
||||
elif n == 5:
|
||||
strval = 'ZXY'
|
||||
elif n == 6:
|
||||
strval = 'ZYX'
|
||||
|
||||
str_values.append(strval)
|
||||
data['data'] = str_values
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def convert_to_animated_data_dict(d, numvals):
|
||||
if d['is_animated']:
|
||||
padval = d['data'][-1]
|
||||
npad = numvals - len(d['data'])
|
||||
if npad > 0:
|
||||
d['data'] = d['data'] + [padval] * npad
|
||||
else:
|
||||
d['is_animated'] = True
|
||||
d['data'] = [d['data']] * numvals
|
||||
|
||||
|
||||
def is_object_keyframe_animated(obj):
|
||||
loc_data = get_vector_property_data_dict(obj, obj, 'location', 3)
|
||||
rot_mode_data = get_rotation_mode_data_dict(obj)
|
||||
euler_rot_data = get_vector_property_data_dict(obj, obj, 'rotation_euler', 3)
|
||||
axis_rot_data = get_vector_property_data_dict(obj, obj, 'rotation_axis_angle', 4)
|
||||
quat_rot_data = get_vector_property_data_dict(obj, obj, 'rotation_quaternion', 4)
|
||||
scale_data = get_vector_property_data_dict(obj, obj, 'scale', 3)
|
||||
|
||||
is_rotation_animated = (euler_rot_data['is_animated'] or
|
||||
axis_rot_data['is_animated'] or
|
||||
quat_rot_data['is_animated'] or
|
||||
rot_mode_data['is_animated'])
|
||||
|
||||
if is_rotation_animated and not rot_mode_data['is_animated']:
|
||||
rot_mode = rot_mode_data['data']
|
||||
if rot_mode == "AXIS_ANGLE":
|
||||
is_rotation_animated = axis_rot_data['is_animated']
|
||||
elif rot_mode == "QUATERNION":
|
||||
is_rotation_animated = quat_rot_data['is_animated']
|
||||
else:
|
||||
is_rotation_animated = euler_rot_data['is_animated']
|
||||
|
||||
is_animated = (loc_data['is_animated'] or
|
||||
is_rotation_animated or
|
||||
scale_data['is_animated'])
|
||||
|
||||
return is_animated
|
||||
|
||||
|
||||
def get_object_transform_data_dict(obj):
|
||||
|
||||
loc_data = get_vector_property_data_dict(obj, obj, 'location', 3)
|
||||
rot_mode_data = get_rotation_mode_data_dict(obj)
|
||||
euler_rot_data = get_vector_property_data_dict(obj, obj, 'rotation_euler', 3)
|
||||
axis_rot_data = get_vector_property_data_dict(obj, obj, 'rotation_axis_angle', 4)
|
||||
quat_rot_data = get_vector_property_data_dict(obj, obj, 'rotation_quaternion', 4)
|
||||
scale_data = get_vector_property_data_dict(obj, obj, 'scale', 3)
|
||||
|
||||
is_rotation_animated = (euler_rot_data['is_animated'] or
|
||||
axis_rot_data['is_animated'] or
|
||||
quat_rot_data['is_animated'] or
|
||||
rot_mode_data['is_animated'])
|
||||
|
||||
if is_rotation_animated and not rot_mode_data['is_animated']:
|
||||
rot_mode = rot_mode_data['data']
|
||||
if rot_mode == "AXIS_ANGLE":
|
||||
is_rotation_animated = axis_rot_data['is_animated']
|
||||
elif rot_mode == "QUATERNION":
|
||||
is_rotation_animated = quat_rot_data['is_animated']
|
||||
else:
|
||||
is_rotation_animated = euler_rot_data['is_animated']
|
||||
|
||||
is_animated = (loc_data['is_animated'] or
|
||||
is_rotation_animated or
|
||||
scale_data['is_animated'])
|
||||
|
||||
if not is_animated:
|
||||
transform = {}
|
||||
transform['location'] = loc_data['data']
|
||||
transform['rotation_mode'] = rot_mode_data['data']
|
||||
if transform['rotation_mode'] == "AXIS_ANGLE":
|
||||
transform['rotation'] = axis_rot_data['data']
|
||||
elif transform['rotation_mode'] == "QUATERNION":
|
||||
transform['rotation'] = quat_rot_data['data']
|
||||
else:
|
||||
transform['rotation'] = euler_rot_data['data']
|
||||
transform['scale'] = scale_data['data']
|
||||
return {'is_animated' : False, 'data' : transform}
|
||||
|
||||
numvals = 0
|
||||
if loc_data['is_animated']:
|
||||
numvals = max(numvals, len(loc_data['data']))
|
||||
if rot_mode_data['is_animated']:
|
||||
numvals = max(numvals, len(rot_mode_data['data']))
|
||||
if euler_rot_data['is_animated']:
|
||||
numvals = max(numvals, len(euler_rot_data['data']))
|
||||
if axis_rot_data['is_animated']:
|
||||
numvals = max(numvals, len(axis_rot_data['data']))
|
||||
if quat_rot_data['is_animated']:
|
||||
numvals = max(numvals, len(quat_rot_data['data']))
|
||||
if scale_data['is_animated']:
|
||||
numvals = max(numvals, len(scale_data['data']))
|
||||
|
||||
convert_to_animated_data_dict(loc_data, numvals)
|
||||
convert_to_animated_data_dict(rot_mode_data, numvals)
|
||||
convert_to_animated_data_dict(euler_rot_data, numvals)
|
||||
convert_to_animated_data_dict(axis_rot_data, numvals)
|
||||
convert_to_animated_data_dict(quat_rot_data, numvals)
|
||||
convert_to_animated_data_dict(scale_data, numvals)
|
||||
|
||||
transforms = []
|
||||
for i in range(numvals):
|
||||
t = {}
|
||||
t['location'] = loc_data['data'][i]
|
||||
t['rotation_mode'] = rot_mode_data['data'][i]
|
||||
if t['rotation_mode'] == "AXIS_ANGLE":
|
||||
t['rotation'] = axis_rot_data['data'][i]
|
||||
elif t['rotation_mode'] == "QUATERNION":
|
||||
t['rotation'] = quat_rot_data['data'][i]
|
||||
else:
|
||||
t['rotation'] = euler_rot_data['data'][i]
|
||||
t['scale'] = scale_data['data'][i]
|
||||
|
||||
transforms.append(t)
|
||||
|
||||
return {'is_animated' : True, 'data' : transforms}
|
||||
|
||||
|
||||
def transform_data_to_world_matrix(transform):
|
||||
mat_loc = Matrix.Translation(transform['location']).to_4x4()
|
||||
|
||||
if transform['rotation_mode'] == "AXIS_ANGLE":
|
||||
angle = transform['rotation'][0]
|
||||
axis = Vector(transform['rotation'][1],
|
||||
transform['rotation'][2],
|
||||
transform['rotation'][3])
|
||||
mat_rot = Matrix.Rotation(angle, 4, axis)
|
||||
elif transform['rotation_mode'] == "QUATERNION":
|
||||
q = Quaternion(transform['rotation'])
|
||||
mat_rot = q.to_matrix().to_4x4()
|
||||
else:
|
||||
e = Euler(transform['rotation'], transform['rotation_mode'])
|
||||
mat_rot = e.to_matrix().to_4x4()
|
||||
|
||||
mat_scale = Matrix.Identity(4)
|
||||
mat_scale[0][0] = transform['scale'][0]
|
||||
mat_scale[1][1] = transform['scale'][1]
|
||||
mat_scale[2][2] = transform['scale'][2]
|
||||
|
||||
return vcu.element_multiply(vcu.element_multiply(mat_loc, mat_rot), mat_scale)
|
||||
|
||||
|
||||
def get_object_world_matrix_data_dict(obj):
|
||||
tdata = get_object_transform_data_dict(obj)
|
||||
|
||||
if tdata['is_animated']:
|
||||
matrices = []
|
||||
for transform in tdata['data']:
|
||||
matrices.append(transform_data_to_world_matrix(transform))
|
||||
return {'is_animated' : True, 'data' : matrices}
|
||||
else:
|
||||
m = transform_data_to_world_matrix(tdata['data'])
|
||||
return {'is_animated' : False, 'data' : m}
|
||||
|
||||
|
||||
def get_object_bbox_center(obj):
|
||||
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
|
||||
global_bbox_center = vcu.element_multiply(obj.matrix_world, local_bbox_center)
|
||||
return global_bbox_center
|
||||
|
||||
|
||||
def get_object_center_data_dict(obj):
|
||||
matdata = get_object_world_matrix_data_dict(obj)
|
||||
orig_matrix_world = obj.matrix_world
|
||||
|
||||
ret_dict = {}
|
||||
if matdata['is_animated']:
|
||||
center_data = []
|
||||
for m in matdata['data']:
|
||||
obj.matrix_world = m
|
||||
center = get_object_bbox_center(obj)
|
||||
center_data.append(center)
|
||||
ret_dict['is_animated'] = True
|
||||
ret_dict['data'] = center_data
|
||||
else:
|
||||
obj.matrix_world = matdata['data']
|
||||
center = get_object_bbox_center(obj)
|
||||
ret_dict['is_animated'] = False
|
||||
ret_dict['data'] = center
|
||||
|
||||
obj.matrix_world = orig_matrix_world
|
||||
vcu.depsgraph_update()
|
||||
|
||||
return ret_dict
|
||||
|
||||
|
||||
# Vector of obj1 towards obj2
|
||||
def get_object_to_object_vector_data_dict(obj1, obj2):
|
||||
cdata1 = get_object_center_data_dict(obj1)
|
||||
cdata2 = get_object_center_data_dict(obj2)
|
||||
|
||||
is_animated = cdata1['is_animated'] or cdata2['is_animated']
|
||||
if is_animated:
|
||||
numvals = 0
|
||||
if cdata1['is_animated']:
|
||||
numvals = max(numvals, len(cdata1['data']))
|
||||
if cdata2['is_animated']:
|
||||
numvals = max(numvals, len(cdata2['data']))
|
||||
|
||||
convert_to_animated_data_dict(cdata1, numvals)
|
||||
convert_to_animated_data_dict(cdata2, numvals)
|
||||
|
||||
vector_data = []
|
||||
for i in range(numvals):
|
||||
v = cdata2['data'][i] - cdata1['data'][i]
|
||||
vector_data.append(v)
|
||||
return {'is_animated' : True, 'data' : vector_data}
|
||||
else:
|
||||
v = cdata2['data'] - cdata1['data']
|
||||
return {'is_animated' : False, 'data' : v}
|
||||
@@ -0,0 +1,285 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy, os, sys, json, datetime
|
||||
|
||||
from . import version_compatibility_utils as vcu
|
||||
|
||||
IS_INSTALLATION_UTILS_INITIALIZED = False
|
||||
IS_INSTALLATION_COMPLETE = False
|
||||
IS_STABLE_BUILD = True
|
||||
|
||||
IS_MIXBOX_SUPPORTED = True
|
||||
IS_MIXBOX_INSTALLATION_COMPLETE = False
|
||||
MIXBOX_BOOST_FACTOR = 1.2
|
||||
|
||||
IS_PRESET_LIBRARY_INSTALLATION_COMPLETE = False
|
||||
PRESET_LIBRARY_INSTALLATIONS = []
|
||||
|
||||
IS_ADDON_ACTIVE = False
|
||||
|
||||
|
||||
def is_installation_complete():
|
||||
global IS_INSTALLATION_UTILS_INITIALIZED
|
||||
global IS_INSTALLATION_COMPLETE
|
||||
|
||||
if IS_INSTALLATION_UTILS_INITIALIZED:
|
||||
return IS_INSTALLATION_COMPLETE
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
addon_dir = os.path.dirname(script_dir)
|
||||
install_file = os.path.join(addon_dir, "resources", "installation_data", "installation_complete")
|
||||
|
||||
if os.path.exists(install_file):
|
||||
IS_INSTALLATION_COMPLETE = True
|
||||
else:
|
||||
IS_INSTALLATION_COMPLETE = False
|
||||
|
||||
IS_INSTALLATION_UTILS_INITIALIZED = True
|
||||
|
||||
return IS_INSTALLATION_COMPLETE
|
||||
|
||||
|
||||
def complete_installation():
|
||||
global IS_INSTALLATION_UTILS_INITIALIZED
|
||||
global IS_INSTALLATION_COMPLETE
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
addon_dir = os.path.dirname(script_dir)
|
||||
install_file = os.path.join(addon_dir, "resources", "installation_data", "installation_complete")
|
||||
|
||||
if not os.path.exists(install_file):
|
||||
with open(install_file, 'w') as f:
|
||||
f.write("1")
|
||||
|
||||
IS_INSTALLATION_COMPLETE = True
|
||||
IS_INSTALLATION_UTILS_INITIALIZED = True
|
||||
|
||||
|
||||
# Avoid using this method in Blender 4.2+
|
||||
def get_module_name():
|
||||
module_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
module_name = os.path.basename(os.path.normpath(module_dir))
|
||||
hardcoded_name = "flip_fluids_addon"
|
||||
if module_name != hardcoded_name:
|
||||
errmsg = "Installation Error Detected"
|
||||
errdesc = "The FLIP Fluids addon directory located at <" + module_dir + "> must be named 'flip_fluids_addon'. Please rename this directory, restart Blender, and re-enable the addon."
|
||||
bpy.ops.flip_fluid_operators.display_error(
|
||||
'INVOKE_DEFAULT',
|
||||
error_message=errmsg,
|
||||
error_description=errdesc,
|
||||
popup_width=600
|
||||
)
|
||||
return hardcoded_name
|
||||
|
||||
|
||||
def get_enabled_flip_fluids_addon_installations():
|
||||
import addon_utils
|
||||
bl_prefs = vcu.get_blender_preferences()
|
||||
|
||||
name_prefix = "FLIP Fluids"
|
||||
description_prefix = "A FLIP Fluid Simulation Tool for Blender"
|
||||
flip_fluids_installations = []
|
||||
for mod_name in bl_prefs.addons.keys():
|
||||
try:
|
||||
module = sys.modules[mod_name]
|
||||
name = module.bl_info.get('name', "")
|
||||
description = module.bl_info.get('description', "")
|
||||
if name.startswith(name_prefix) and description.startswith(description_prefix):
|
||||
d = {}
|
||||
d['module_name'] = mod_name
|
||||
d['addon_name'] = name
|
||||
flip_fluids_installations.append(d)
|
||||
except:
|
||||
pass
|
||||
|
||||
return flip_fluids_installations
|
||||
|
||||
|
||||
def is_experimental_build():
|
||||
global IS_STABLE_BUILD
|
||||
return not IS_STABLE_BUILD
|
||||
|
||||
|
||||
def is_addon_active():
|
||||
global IS_ADDON_ACTIVE
|
||||
return IS_ADDON_ACTIVE
|
||||
|
||||
|
||||
def tag_addon_active():
|
||||
global IS_ADDON_ACTIVE
|
||||
IS_ADDON_ACTIVE = True
|
||||
|
||||
|
||||
def tag_addon_inactive():
|
||||
global IS_ADDON_ACTIVE
|
||||
IS_ADDON_ACTIVE = False
|
||||
|
||||
|
||||
def update_mixbox_installation_status():
|
||||
global IS_MIXBOX_INSTALLATION_COMPLETE
|
||||
if not is_mixbox_supported():
|
||||
IS_MIXBOX_INSTALLATION_COMPLETE = False
|
||||
return
|
||||
|
||||
lut_filename = "mixbox_lut_data.bin"
|
||||
addon_directory = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
mixbox_lut_filepath = os.path.join(addon_directory, "third_party", "mixbox", lut_filename)
|
||||
IS_MIXBOX_INSTALLATION_COMPLETE = os.path.isfile(mixbox_lut_filepath)
|
||||
return IS_MIXBOX_INSTALLATION_COMPLETE
|
||||
|
||||
|
||||
def is_mixbox_supported():
|
||||
global IS_MIXBOX_SUPPORTED
|
||||
return IS_MIXBOX_SUPPORTED
|
||||
|
||||
|
||||
def is_mixbox_installation_complete():
|
||||
global IS_MIXBOX_INSTALLATION_COMPLETE
|
||||
return IS_MIXBOX_INSTALLATION_COMPLETE
|
||||
|
||||
|
||||
def get_mixbox_boost_factor():
|
||||
global MIXBOX_BOOST_FACTOR
|
||||
return MIXBOX_BOOST_FACTOR
|
||||
|
||||
|
||||
def update_preset_library_installation_status():
|
||||
global IS_PRESET_LIBRARY_INSTALLATION_COMPLETE
|
||||
global PRESET_LIBRARY_INSTALLATIONS
|
||||
|
||||
IS_PRESET_LIBRARY_INSTALLATION_COMPLETE = False
|
||||
PRESET_LIBRARY_INSTALLATIONS = []
|
||||
|
||||
if not vcu.is_blender_33():
|
||||
# Asset library is only available in Blender 3.3 or later
|
||||
return
|
||||
|
||||
bl_preferences = bpy.context.preferences
|
||||
bl_filepaths = bl_preferences.filepaths
|
||||
for lib_entry in bl_filepaths.asset_libraries:
|
||||
entry_name = lib_entry.name
|
||||
entry_path = lib_entry.path
|
||||
expected_metadata_filepath = os.path.join(entry_path, ".metadata", "version.json")
|
||||
if not os.path.isfile(expected_metadata_filepath):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(expected_metadata_filepath, 'r') as version_file:
|
||||
version_data = json.loads(version_file.read())
|
||||
d = {}
|
||||
d["name"] = entry_name
|
||||
d["path"] = entry_path
|
||||
d["install_path"] = os.path.dirname(entry_path)
|
||||
d["metadata"] = version_data
|
||||
PRESET_LIBRARY_INSTALLATIONS.append(d)
|
||||
IS_PRESET_LIBRARY_INSTALLATION_COMPLETE = True
|
||||
except Exception as e:
|
||||
print("FLIP Fluids: Error verifying preset library install. <" + lib_entry.name + " : " + entry_path + ">" + "<" + str(e) + ">")
|
||||
|
||||
return IS_PRESET_LIBRARY_INSTALLATION_COMPLETE
|
||||
|
||||
|
||||
def is_preset_library_installation_complete():
|
||||
global IS_PRESET_LIBRARY_INSTALLATION_COMPLETE
|
||||
return IS_PRESET_LIBRARY_INSTALLATION_COMPLETE
|
||||
|
||||
|
||||
def get_preset_library_installations():
|
||||
global PRESET_LIBRARY_INSTALLATIONS
|
||||
return PRESET_LIBRARY_INSTALLATIONS
|
||||
|
||||
|
||||
def is_turbo_tools_addon_enabled():
|
||||
# Used for command line tools to launch Turbo Tools Render processes
|
||||
# https://blendermarket.com/products/turbo-tools-v3---turbo-render-turbo-comp-temporal-stabilizer
|
||||
return "Turbo Tools" in vcu.get_blender_preferences().addons
|
||||
|
||||
|
||||
def get_compiler_info_list():
|
||||
addon_directory = os.path.dirname(os.path.dirname(__file__))
|
||||
compiler_data_directory = os.path.join(addon_directory, "resources", "compiler_data")
|
||||
if not os.path.isdir(compiler_data_directory):
|
||||
return []
|
||||
|
||||
compiler_info = []
|
||||
for filename in os.listdir(compiler_data_directory):
|
||||
filepath = os.path.join(compiler_data_directory, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
if filename == "empty_file":
|
||||
continue
|
||||
|
||||
with open(filepath, 'r') as f:
|
||||
file_text = f.read()
|
||||
file_text = file_text.replace("%z", "local")
|
||||
compiler_info.append(file_text)
|
||||
|
||||
return compiler_info
|
||||
|
||||
|
||||
def get_library_list():
|
||||
addon_directory = os.path.dirname(os.path.dirname(__file__))
|
||||
library_directory = os.path.join(addon_directory, "ffengine", "lib")
|
||||
if not os.path.isdir(library_directory):
|
||||
return []
|
||||
|
||||
library_list = []
|
||||
for filename in os.listdir(library_directory):
|
||||
filepath = os.path.join(library_directory, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
date_modified = os.path.getmtime(filepath)
|
||||
date_modified = datetime.datetime.fromtimestamp(date_modified, tz=datetime.timezone.utc)
|
||||
date_string = date_modified.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
library_entry = filename + " <" + date_string + ">"
|
||||
library_list.append(library_entry)
|
||||
|
||||
return library_list
|
||||
|
||||
|
||||
def __load_post_update_is_addon_active():
|
||||
tag_addon_inactive()
|
||||
if bpy.context.scene.flip_fluid.is_domain_object_set():
|
||||
tag_addon_active()
|
||||
return
|
||||
if bpy.context.scene.flip_fluid.get_simulation_objects():
|
||||
tag_addon_active()
|
||||
return
|
||||
|
||||
|
||||
def load_post():
|
||||
complete_installation()
|
||||
update_mixbox_installation_status()
|
||||
__load_post_update_is_addon_active()
|
||||
|
||||
|
||||
def scene_update_post(scene):
|
||||
if is_addon_active():
|
||||
return
|
||||
|
||||
# FLIP Fluid objects appended from another scene will not tag the addon as active
|
||||
# This workaround is to quickly check whether the active object is a FLIP Fluid object
|
||||
# and in this case, tag the addon as active
|
||||
try:
|
||||
active_object = vcu.get_active_object()
|
||||
except AttributeError as e:
|
||||
# Context may not have an active object available, such as when exporting to Alembic.
|
||||
# In this case, ignore and return.
|
||||
return
|
||||
|
||||
if active_object is not None and active_object.flip_fluid.is_active:
|
||||
tag_addon_active()
|
||||
@@ -0,0 +1,285 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy, os, sys, json, datetime
|
||||
|
||||
from . import version_compatibility_utils as vcu
|
||||
|
||||
IS_INSTALLATION_UTILS_INITIALIZED = False
|
||||
IS_INSTALLATION_COMPLETE = False
|
||||
IS_STABLE_BUILD = @FLUIDENGINE_VERSION_TYPE_IS_STABLE_BUILD_PYTHON@
|
||||
|
||||
IS_MIXBOX_SUPPORTED = @FLUIDENGINE_IS_MIXBOX_SUPPORTED@
|
||||
IS_MIXBOX_INSTALLATION_COMPLETE = False
|
||||
MIXBOX_BOOST_FACTOR = 1.2
|
||||
|
||||
IS_PRESET_LIBRARY_INSTALLATION_COMPLETE = False
|
||||
PRESET_LIBRARY_INSTALLATIONS = []
|
||||
|
||||
IS_ADDON_ACTIVE = False
|
||||
|
||||
|
||||
def is_installation_complete():
|
||||
global IS_INSTALLATION_UTILS_INITIALIZED
|
||||
global IS_INSTALLATION_COMPLETE
|
||||
|
||||
if IS_INSTALLATION_UTILS_INITIALIZED:
|
||||
return IS_INSTALLATION_COMPLETE
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
addon_dir = os.path.dirname(script_dir)
|
||||
install_file = os.path.join(addon_dir, "resources", "installation_data", "installation_complete")
|
||||
|
||||
if os.path.exists(install_file):
|
||||
IS_INSTALLATION_COMPLETE = True
|
||||
else:
|
||||
IS_INSTALLATION_COMPLETE = False
|
||||
|
||||
IS_INSTALLATION_UTILS_INITIALIZED = True
|
||||
|
||||
return IS_INSTALLATION_COMPLETE
|
||||
|
||||
|
||||
def complete_installation():
|
||||
global IS_INSTALLATION_UTILS_INITIALIZED
|
||||
global IS_INSTALLATION_COMPLETE
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
addon_dir = os.path.dirname(script_dir)
|
||||
install_file = os.path.join(addon_dir, "resources", "installation_data", "installation_complete")
|
||||
|
||||
if not os.path.exists(install_file):
|
||||
with open(install_file, 'w') as f:
|
||||
f.write("1")
|
||||
|
||||
IS_INSTALLATION_COMPLETE = True
|
||||
IS_INSTALLATION_UTILS_INITIALIZED = True
|
||||
|
||||
|
||||
# Avoid using this method in Blender 4.2+
|
||||
def get_module_name():
|
||||
module_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
module_name = os.path.basename(os.path.normpath(module_dir))
|
||||
hardcoded_name = "flip_fluids_addon"
|
||||
if module_name != hardcoded_name:
|
||||
errmsg = "Installation Error Detected"
|
||||
errdesc = "The FLIP Fluids addon directory located at <" + module_dir + "> must be named 'flip_fluids_addon'. Please rename this directory, restart Blender, and re-enable the addon."
|
||||
bpy.ops.flip_fluid_operators.display_error(
|
||||
'INVOKE_DEFAULT',
|
||||
error_message=errmsg,
|
||||
error_description=errdesc,
|
||||
popup_width=600
|
||||
)
|
||||
return hardcoded_name
|
||||
|
||||
|
||||
def get_enabled_flip_fluids_addon_installations():
|
||||
import addon_utils
|
||||
bl_prefs = vcu.get_blender_preferences()
|
||||
|
||||
name_prefix = "FLIP Fluids"
|
||||
description_prefix = "A FLIP Fluid Simulation Tool for Blender"
|
||||
flip_fluids_installations = []
|
||||
for mod_name in bl_prefs.addons.keys():
|
||||
try:
|
||||
module = sys.modules[mod_name]
|
||||
name = module.bl_info.get('name', "")
|
||||
description = module.bl_info.get('description', "")
|
||||
if name.startswith(name_prefix) and description.startswith(description_prefix):
|
||||
d = {}
|
||||
d['module_name'] = mod_name
|
||||
d['addon_name'] = name
|
||||
flip_fluids_installations.append(d)
|
||||
except:
|
||||
pass
|
||||
|
||||
return flip_fluids_installations
|
||||
|
||||
|
||||
def is_experimental_build():
|
||||
global IS_STABLE_BUILD
|
||||
return not IS_STABLE_BUILD
|
||||
|
||||
|
||||
def is_addon_active():
|
||||
global IS_ADDON_ACTIVE
|
||||
return IS_ADDON_ACTIVE
|
||||
|
||||
|
||||
def tag_addon_active():
|
||||
global IS_ADDON_ACTIVE
|
||||
IS_ADDON_ACTIVE = True
|
||||
|
||||
|
||||
def tag_addon_inactive():
|
||||
global IS_ADDON_ACTIVE
|
||||
IS_ADDON_ACTIVE = False
|
||||
|
||||
|
||||
def update_mixbox_installation_status():
|
||||
global IS_MIXBOX_INSTALLATION_COMPLETE
|
||||
if not is_mixbox_supported():
|
||||
IS_MIXBOX_INSTALLATION_COMPLETE = False
|
||||
return
|
||||
|
||||
lut_filename = "mixbox_lut_data.bin"
|
||||
addon_directory = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
mixbox_lut_filepath = os.path.join(addon_directory, "third_party", "mixbox", lut_filename)
|
||||
IS_MIXBOX_INSTALLATION_COMPLETE = os.path.isfile(mixbox_lut_filepath)
|
||||
return IS_MIXBOX_INSTALLATION_COMPLETE
|
||||
|
||||
|
||||
def is_mixbox_supported():
|
||||
global IS_MIXBOX_SUPPORTED
|
||||
return IS_MIXBOX_SUPPORTED
|
||||
|
||||
|
||||
def is_mixbox_installation_complete():
|
||||
global IS_MIXBOX_INSTALLATION_COMPLETE
|
||||
return IS_MIXBOX_INSTALLATION_COMPLETE
|
||||
|
||||
|
||||
def get_mixbox_boost_factor():
|
||||
global MIXBOX_BOOST_FACTOR
|
||||
return MIXBOX_BOOST_FACTOR
|
||||
|
||||
|
||||
def update_preset_library_installation_status():
|
||||
global IS_PRESET_LIBRARY_INSTALLATION_COMPLETE
|
||||
global PRESET_LIBRARY_INSTALLATIONS
|
||||
|
||||
IS_PRESET_LIBRARY_INSTALLATION_COMPLETE = False
|
||||
PRESET_LIBRARY_INSTALLATIONS = []
|
||||
|
||||
if not vcu.is_blender_33():
|
||||
# Asset library is only available in Blender 3.3 or later
|
||||
return
|
||||
|
||||
bl_preferences = bpy.context.preferences
|
||||
bl_filepaths = bl_preferences.filepaths
|
||||
for lib_entry in bl_filepaths.asset_libraries:
|
||||
entry_name = lib_entry.name
|
||||
entry_path = lib_entry.path
|
||||
expected_metadata_filepath = os.path.join(entry_path, ".metadata", "version.json")
|
||||
if not os.path.isfile(expected_metadata_filepath):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(expected_metadata_filepath, 'r') as version_file:
|
||||
version_data = json.loads(version_file.read())
|
||||
d = {}
|
||||
d["name"] = entry_name
|
||||
d["path"] = entry_path
|
||||
d["install_path"] = os.path.dirname(entry_path)
|
||||
d["metadata"] = version_data
|
||||
PRESET_LIBRARY_INSTALLATIONS.append(d)
|
||||
IS_PRESET_LIBRARY_INSTALLATION_COMPLETE = True
|
||||
except Exception as e:
|
||||
print("FLIP Fluids: Error verifying preset library install. <" + lib_entry.name + " : " + entry_path + ">" + "<" + str(e) + ">")
|
||||
|
||||
return IS_PRESET_LIBRARY_INSTALLATION_COMPLETE
|
||||
|
||||
|
||||
def is_preset_library_installation_complete():
|
||||
global IS_PRESET_LIBRARY_INSTALLATION_COMPLETE
|
||||
return IS_PRESET_LIBRARY_INSTALLATION_COMPLETE
|
||||
|
||||
|
||||
def get_preset_library_installations():
|
||||
global PRESET_LIBRARY_INSTALLATIONS
|
||||
return PRESET_LIBRARY_INSTALLATIONS
|
||||
|
||||
|
||||
def is_turbo_tools_addon_enabled():
|
||||
# Used for command line tools to launch Turbo Tools Render processes
|
||||
# https://blendermarket.com/products/turbo-tools-v3---turbo-render-turbo-comp-temporal-stabilizer
|
||||
return "Turbo Tools" in vcu.get_blender_preferences().addons
|
||||
|
||||
|
||||
def get_compiler_info_list():
|
||||
addon_directory = os.path.dirname(os.path.dirname(__file__))
|
||||
compiler_data_directory = os.path.join(addon_directory, "resources", "compiler_data")
|
||||
if not os.path.isdir(compiler_data_directory):
|
||||
return []
|
||||
|
||||
compiler_info = []
|
||||
for filename in os.listdir(compiler_data_directory):
|
||||
filepath = os.path.join(compiler_data_directory, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
if filename == "empty_file":
|
||||
continue
|
||||
|
||||
with open(filepath, 'r') as f:
|
||||
file_text = f.read()
|
||||
file_text = file_text.replace("%z", "local")
|
||||
compiler_info.append(file_text)
|
||||
|
||||
return compiler_info
|
||||
|
||||
|
||||
def get_library_list():
|
||||
addon_directory = os.path.dirname(os.path.dirname(__file__))
|
||||
library_directory = os.path.join(addon_directory, "ffengine", "lib")
|
||||
if not os.path.isdir(library_directory):
|
||||
return []
|
||||
|
||||
library_list = []
|
||||
for filename in os.listdir(library_directory):
|
||||
filepath = os.path.join(library_directory, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
date_modified = os.path.getmtime(filepath)
|
||||
date_modified = datetime.datetime.fromtimestamp(date_modified, tz=datetime.timezone.utc)
|
||||
date_string = date_modified.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
library_entry = filename + " <" + date_string + ">"
|
||||
library_list.append(library_entry)
|
||||
|
||||
return library_list
|
||||
|
||||
|
||||
def __load_post_update_is_addon_active():
|
||||
tag_addon_inactive()
|
||||
if bpy.context.scene.flip_fluid.is_domain_object_set():
|
||||
tag_addon_active()
|
||||
return
|
||||
if bpy.context.scene.flip_fluid.get_simulation_objects():
|
||||
tag_addon_active()
|
||||
return
|
||||
|
||||
|
||||
def load_post():
|
||||
complete_installation()
|
||||
update_mixbox_installation_status()
|
||||
__load_post_update_is_addon_active()
|
||||
|
||||
|
||||
def scene_update_post(scene):
|
||||
if is_addon_active():
|
||||
return
|
||||
|
||||
# FLIP Fluid objects appended from another scene will not tag the addon as active
|
||||
# This workaround is to quickly check whether the active object is a FLIP Fluid object
|
||||
# and in this case, tag the addon as active
|
||||
try:
|
||||
active_object = vcu.get_active_object()
|
||||
except AttributeError as e:
|
||||
# Context may not have an active object available, such as when exporting to Alembic.
|
||||
# In this case, ignore and return.
|
||||
return
|
||||
|
||||
if active_object is not None and active_object.flip_fluid.is_active:
|
||||
tag_addon_active()
|
||||
@@ -0,0 +1,80 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy, os, glob
|
||||
|
||||
|
||||
def load_preset_materials(preset_info, loaded_materials_info):
|
||||
preset_directory = preset_info['path']
|
||||
blend_files = glob.glob(preset_directory + "/*.blend")
|
||||
if not blend_files:
|
||||
return
|
||||
|
||||
for blend_file in blend_files:
|
||||
material_names_before = []
|
||||
material_names_after = []
|
||||
is_material_owner = []
|
||||
with bpy.data.libraries.load(blend_file) as (data_from, data_to):
|
||||
for blend_material_name in data_from.materials:
|
||||
material_names_before.append(blend_material_name)
|
||||
material_object = _find_preset_material(preset_info['identifier'], blend_material_name)
|
||||
if material_object is not None:
|
||||
material_names_after.append(material_object.name)
|
||||
is_material_owner.append(False)
|
||||
else:
|
||||
data_to.materials.append(blend_material_name)
|
||||
material_names_after.append(None)
|
||||
is_material_owner.append(True)
|
||||
|
||||
for idx,material_object in enumerate(data_to.materials):
|
||||
material_object.flip_fluid_material_library.is_preset_material = True
|
||||
material_object.flip_fluid_material_library.preset_identifier = preset_info['identifier']
|
||||
material_object.flip_fluid_material_library.preset_blend_identifier = material_names_before[idx]
|
||||
for nidx,material_name in enumerate(material_names_after):
|
||||
if material_name is None:
|
||||
material_names_after[nidx] = material_object.name
|
||||
break
|
||||
|
||||
for i in range(len(material_names_before)):
|
||||
minfo = loaded_materials_info.add()
|
||||
minfo.preset_id = material_names_before[i]
|
||||
minfo.loaded_id = material_names_after[i]
|
||||
if hasattr(minfo, "is_owner"):
|
||||
minfo.is_owner = is_material_owner[i]
|
||||
|
||||
|
||||
def unload_preset_materials(loaded_materials_info):
|
||||
for minfo in loaded_materials_info:
|
||||
material = bpy.data.materials.get(minfo.loaded_id)
|
||||
if hasattr(minfo, "is_owner"):
|
||||
remove_material = material is not None and minfo.is_owner
|
||||
else:
|
||||
if material.flip_fluid_material_library.is_fake_user_set_by_addon and material.use_fake_user:
|
||||
material.use_fake_user = False
|
||||
material.flip_fluid_material_library.is_fake_user_set_by_addon = False
|
||||
remove_material = material is not None and material.users == 0
|
||||
if remove_material and not material.flip_fluid_material_library.skip_preset_unload:
|
||||
bpy.data.materials.remove(material)
|
||||
|
||||
|
||||
def _find_preset_material(preset_id, material_id):
|
||||
for m in bpy.data.materials:
|
||||
if not m.flip_fluid_material_library.is_preset_material:
|
||||
continue
|
||||
if (m.flip_fluid_material_library.preset_identifier == preset_id and
|
||||
m.flip_fluid_material_library.preset_blend_identifier == material_id):
|
||||
return m
|
||||
return None
|
||||
@@ -0,0 +1,163 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
|
||||
from . import version_compatibility_utils as vcu
|
||||
|
||||
def partition_chunks_column3(chunks):
|
||||
"""
|
||||
Partition a list a chunks into three columns in a way that minimizes the
|
||||
maximum height of all columns
|
||||
|
||||
chunk = {
|
||||
'label': Display label text,
|
||||
'size': number of elements in chunk,
|
||||
'collection': collection that this chunk is associated with,
|
||||
'is_continuation': whether this chunk is a continuation of a
|
||||
previous chunk,
|
||||
'id': index that this chunk belongs to
|
||||
}
|
||||
"""
|
||||
n = len(chunks)
|
||||
if n <= 3:
|
||||
partitions = [[], [], []]
|
||||
for i in range(n):
|
||||
partitions[i] = [chunks[i]]
|
||||
else:
|
||||
current_min = 1e6
|
||||
min_candidates = []
|
||||
for i in range(1, n - 1):
|
||||
for j in range(i + 1, n):
|
||||
sum1 = sum(chunks[x]['size'] for x in range(0, i))
|
||||
sum2 = sum(chunks[x]['size'] for x in range(i, j))
|
||||
sum3 = sum(chunks[x]['size'] for x in range(j, n))
|
||||
maxsum = max(sum1, sum2, sum3)
|
||||
if maxsum <= current_min:
|
||||
if maxsum < current_min:
|
||||
current_min = maxsum
|
||||
min_candidates = []
|
||||
candidate = {
|
||||
'score': maxsum, 'sums': [sum1, sum2, sum3], 'i': i, 'j': j
|
||||
}
|
||||
min_candidates.append(candidate)
|
||||
|
||||
min_range = 1e6
|
||||
winner = None
|
||||
final_candidates = []
|
||||
for m in min_candidates:
|
||||
sum_range = max(m['sums']) - min(m['sums'])
|
||||
if sum_range <= min_range:
|
||||
if sum_range < current_min:
|
||||
min_range = sum_range
|
||||
final_candidates = []
|
||||
final_candidates.append(m)
|
||||
|
||||
winner = final_candidates[0]
|
||||
for m in final_candidates:
|
||||
if (m['sums'][0] <= m['sums'][1]) and (m['sums'][1] <= m['sums'][2]):
|
||||
winner = m
|
||||
|
||||
p1 = [chunks[x] for x in range(0, winner['i'])]
|
||||
p2 = [chunks[x] for x in range(winner['i'], winner['j'])]
|
||||
p3 = [chunks[x] for x in range(winner['j'], n)]
|
||||
partitions = [p1, p2, p3]
|
||||
|
||||
merged_partitions = []
|
||||
for p in partitions:
|
||||
if not p:
|
||||
merged_partitions.append([])
|
||||
continue
|
||||
|
||||
newp = []
|
||||
current_chunk = p[0]
|
||||
for i in range(1, len(p)):
|
||||
next_chunk = p[i]
|
||||
if next_chunk['id'] == current_chunk['id']:
|
||||
current_chunk['size'] += next_chunk['size']
|
||||
else:
|
||||
newp.append(current_chunk)
|
||||
current_chunk = next_chunk
|
||||
newp.append(current_chunk)
|
||||
merged_partitions.append(newp)
|
||||
partitions = merged_partitions
|
||||
|
||||
all_chunks = partitions[0] + partitions[1] + partitions[2]
|
||||
for i, c in enumerate(all_chunks):
|
||||
if c['is_continuation']:
|
||||
last_c = all_chunks[i - 1]
|
||||
c['range'] = [last_c['range'][1], last_c['range'][1] + c['size']]
|
||||
else:
|
||||
c['range'] = [0, c['size']]
|
||||
|
||||
del c['size']
|
||||
del c['id']
|
||||
|
||||
return partitions
|
||||
|
||||
|
||||
def get_domain_panel_enums_from_paths(property_paths):
|
||||
enabled_panels = {
|
||||
"simulation": False,
|
||||
"render": False,
|
||||
"surface": False,
|
||||
"whitewater": False,
|
||||
"world": False,
|
||||
"materials": False,
|
||||
"advanced": False,
|
||||
"debug": False,
|
||||
"stats": False
|
||||
}
|
||||
|
||||
for path in property_paths:
|
||||
split = path.split('.')
|
||||
enabled_panels[split[1]] = True
|
||||
|
||||
enums = []
|
||||
if enabled_panels['simulation']:
|
||||
enums.append(('simulation', "Simulation", "FLIP Fluid Simulation", 1))
|
||||
if enabled_panels['render']:
|
||||
enums.append(('render', "Display", "FLIP Fluid Display Settings", 2))
|
||||
if enabled_panels['surface']:
|
||||
enums.append(('surface', "Surface", "FLIP Fluid Surface", 3))
|
||||
if enabled_panels['whitewater']:
|
||||
enums.append(('whitewater', "Whitewater", "FLIP Fluid Whitewater", 4))
|
||||
if enabled_panels['world']:
|
||||
enums.append(('world', "World", "FLIP Fluid World", 5))
|
||||
if enabled_panels['materials']:
|
||||
enums.append(('materials', "Materials", "FLIP Fluid Materials", 6))
|
||||
if enabled_panels['advanced']:
|
||||
enums.append(('advanced', "Advanced", "FLIP Fluid Advanced", 7))
|
||||
if enabled_panels['debug']:
|
||||
enums.append(('debug', "Debug", "FLIP Fluid Debug", 8))
|
||||
if enabled_panels['stats']:
|
||||
enums.append(('stats', "Stats", "FLIP Fluid Stats", 9))
|
||||
|
||||
if not enums:
|
||||
enums.append(('NONE', "None", "No panel to select", 0))
|
||||
return enums
|
||||
|
||||
def force_ui_redraw():
|
||||
"""
|
||||
Create an object and then immediately remove it
|
||||
This is a hack to force the ui and 3D viewport to redraw
|
||||
"""
|
||||
mesh_data = bpy.data.meshes.new("test_mesh_data")
|
||||
mesh_data.from_pydata([], [], [])
|
||||
obj = bpy.data.objects.new("test_object", mesh_data)
|
||||
vcu.link_object(obj, bpy.context)
|
||||
vcu.delete_object(obj)
|
||||
|
||||
@@ -0,0 +1,595 @@
|
||||
# Blender FLIP Fluids Add-on
|
||||
# Copyright (C) 2025 Ryan L. Guy & Dennis Fassbaender
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy, array, os, numpy
|
||||
|
||||
from ..ffengine import TriangleMesh
|
||||
|
||||
def is_blender_279():
|
||||
return bpy.app.version <= (2, 79, 999)
|
||||
|
||||
def is_blender_28():
|
||||
return bpy.app.version >= (2, 80, 0)
|
||||
|
||||
|
||||
def is_blender_281():
|
||||
return bpy.app.version >= (2, 81, 0)
|
||||
|
||||
|
||||
def is_blender_282():
|
||||
return bpy.app.version >= (2, 82, 0)
|
||||
|
||||
|
||||
def is_blender_29():
|
||||
return bpy.app.version >= (2, 90, 0)
|
||||
|
||||
|
||||
def is_blender_293():
|
||||
return bpy.app.version >= (2, 93, 0)
|
||||
|
||||
|
||||
def is_blender_30():
|
||||
return bpy.app.version >= (3, 0, 0)
|
||||
|
||||
|
||||
def is_blender_31():
|
||||
return bpy.app.version >= (3, 1, 0)
|
||||
|
||||
|
||||
def is_blender_32():
|
||||
return bpy.app.version >= (3, 2, 0)
|
||||
|
||||
|
||||
def is_blender_33():
|
||||
return bpy.app.version >= (3, 3, 0)
|
||||
|
||||
|
||||
def is_blender_34():
|
||||
return bpy.app.version >= (3, 4, 0)
|
||||
|
||||
|
||||
def is_blender_35():
|
||||
return bpy.app.version >= (3, 5, 0)
|
||||
|
||||
|
||||
def is_blender_36():
|
||||
return bpy.app.version >= (3, 6, 0)
|
||||
|
||||
|
||||
def is_blender_40():
|
||||
return bpy.app.version >= (4, 0, 0)
|
||||
|
||||
|
||||
def is_blender_42():
|
||||
return bpy.app.version >= (4, 2, 0)
|
||||
|
||||
|
||||
def is_blender_43():
|
||||
return bpy.app.version >= (4, 3, 0)
|
||||
|
||||
|
||||
def is_blender_44():
|
||||
return bpy.app.version >= (4, 4, 0)
|
||||
|
||||
|
||||
def is_blender_45():
|
||||
return bpy.app.version >= (4, 5, 0)
|
||||
|
||||
|
||||
def register_dict_property(dict_object, name_str, prop):
|
||||
if is_blender_28():
|
||||
# must use exec as the statement will result in invalid syntax
|
||||
# if script is run in Python versions that do nupport annotation syntax
|
||||
exec("dict_object[name_str]: prop")
|
||||
else:
|
||||
dict_object[name_str] = prop
|
||||
|
||||
|
||||
def convert_attribute_to_28(prop_name):
|
||||
if is_blender_28():
|
||||
p = prop_name
|
||||
return "temp_prop = " + p + "; del " + p + "; " + p + ": temp_prop; del temp_prop"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def get_active_object(context=None):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
return context.active_object
|
||||
else:
|
||||
return context.scene.objects.active
|
||||
|
||||
|
||||
def set_active_object(obj, context=None):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
context.view_layer.objects.active = obj
|
||||
else:
|
||||
context.scene.objects.active = obj
|
||||
|
||||
|
||||
def select_get(obj):
|
||||
if is_blender_28():
|
||||
return obj.select_get()
|
||||
else:
|
||||
return obj.select
|
||||
|
||||
|
||||
def select_set(obj, boolval):
|
||||
if is_blender_28():
|
||||
obj.select_set(boolval)
|
||||
else:
|
||||
obj.select = boolval
|
||||
|
||||
|
||||
|
||||
def get_object_display_type(obj):
|
||||
if is_blender_28():
|
||||
return obj.display_type
|
||||
else:
|
||||
return obj.draw_type
|
||||
|
||||
|
||||
def set_object_display_type(obj, display_type):
|
||||
if is_blender_28():
|
||||
obj.display_type = display_type
|
||||
else:
|
||||
obj.draw_type = display_type
|
||||
|
||||
|
||||
def set_object_hide_viewport(obj, display_bool):
|
||||
if is_blender_28():
|
||||
if obj.hide_get() != display_bool:
|
||||
obj.hide_set(display_bool)
|
||||
else:
|
||||
if obj.hide != display_bool:
|
||||
obj.hide = display_bool
|
||||
|
||||
|
||||
def get_object_hide_viewport(obj):
|
||||
if is_blender_28():
|
||||
return obj.hide_get()
|
||||
else:
|
||||
return obj.hide
|
||||
|
||||
|
||||
def toggle_outline_eye_icon(obj):
|
||||
if is_blender_28():
|
||||
obj.hide_viewport = not obj.hide_viewport
|
||||
else:
|
||||
obj.hide = not obj.hide
|
||||
|
||||
|
||||
def set_object_instance_type(obj, display_type):
|
||||
if is_blender_28():
|
||||
if obj.instance_type != display_type:
|
||||
obj.instance_type = display_type
|
||||
else:
|
||||
if obj.dupli_type != display_type:
|
||||
obj.dupli_type = display_type
|
||||
|
||||
|
||||
def get_flip_fluids_collection(context):
|
||||
collection = bpy.data.collections.get("FLIPFluids")
|
||||
if collection is None:
|
||||
collection = bpy.data.collections.new('FLIPFluids')
|
||||
context.scene.collection.children.link(collection)
|
||||
return collection
|
||||
|
||||
|
||||
def get_flip_mesh_collection(context):
|
||||
mesh_collection = bpy.data.collections.get("FLIPMeshes")
|
||||
if mesh_collection is None:
|
||||
flip_collection = get_flip_fluids_collection(context)
|
||||
mesh_collection = bpy.data.collections.new('FLIPMeshes')
|
||||
flip_collection.children.link(mesh_collection)
|
||||
return mesh_collection
|
||||
|
||||
|
||||
def link_fluid_mesh_object(obj, context=None):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
mesh_collection = get_flip_mesh_collection(context)
|
||||
mesh_collection.objects.link(obj)
|
||||
else:
|
||||
context.scene.objects.link(obj)
|
||||
|
||||
|
||||
def link_object(obj, context=None):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
flip_collection = get_flip_fluids_collection(context)
|
||||
flip_collection.objects.link(obj)
|
||||
else:
|
||||
context.scene.objects.link(obj)
|
||||
|
||||
|
||||
def link_object_to_master_scene(obj, context=None):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
context.scene.collection.objects.link(obj)
|
||||
else:
|
||||
context.scene.objects.link(obj)
|
||||
|
||||
|
||||
def add_to_flip_fluids_collection(obj, context):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
flip_collection = get_flip_fluids_collection(context)
|
||||
if flip_collection.objects.get(obj.name):
|
||||
return
|
||||
flip_collection.objects.link(obj)
|
||||
|
||||
|
||||
def remove_from_flip_fluids_collection(obj, context):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
flip_collection = get_flip_fluids_collection(context)
|
||||
|
||||
num_collections = 0
|
||||
for collection in bpy.data.collections:
|
||||
if collection.name.startswith("RigidBodyWorld"):
|
||||
# The RigidBodyWorld collection (for RBD objects) is more hidden within the Blend file
|
||||
# and may not be apparent to many users. Ignore this collection in the count so that
|
||||
# it does not appear that the objects dissappear.
|
||||
continue
|
||||
if collection.objects.get(obj.name):
|
||||
num_collections += 1
|
||||
if num_collections == 1 and context.scene.collection.objects.get(obj.name) is None:
|
||||
context.scene.collection.objects.link(obj)
|
||||
|
||||
if flip_collection.objects.get(obj.name):
|
||||
flip_collection.objects.unlink(obj)
|
||||
|
||||
|
||||
def delete_object(obj, remove_mesh_data=True):
|
||||
if obj.type == 'MESH':
|
||||
mesh_data = obj.data
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
if remove_mesh_data:
|
||||
mesh_data.user_clear()
|
||||
bpy.data.meshes.remove(mesh_data)
|
||||
else:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
|
||||
def delete_mesh_data(mesh_data):
|
||||
mesh_data.user_clear()
|
||||
bpy.data.meshes.remove(mesh_data)
|
||||
|
||||
|
||||
def get_scene_collection(context=None):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
return context.scene.collection
|
||||
else:
|
||||
return context.scene
|
||||
|
||||
|
||||
def get_all_scene_objects(context=None):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
return context.scene.collection.all_objects
|
||||
else:
|
||||
return context.scene.objects
|
||||
|
||||
|
||||
def element_multiply(v1, v2):
|
||||
if is_blender_28():
|
||||
return v1 @ v2
|
||||
else:
|
||||
return v1 * v2
|
||||
|
||||
|
||||
def depsgraph_update(context=None):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
depsgraph.update()
|
||||
else:
|
||||
context.scene.update()
|
||||
|
||||
|
||||
def object_to_triangle_mesh(obj, matrix_world=None):
|
||||
is_b3d_28 = is_blender_28()
|
||||
|
||||
# To ensure the modifier stack is processed in 2.8, the object's 'hide in viewport'
|
||||
# must be False. This is a limitation of how meshes or exported in Blender.
|
||||
# The 'hide in viewport' status will be set back to the original value at the
|
||||
# end of this method.
|
||||
#
|
||||
# More info: https://developer.blender.org/T71556
|
||||
if is_b3d_28:
|
||||
hide_viewport_status = obj.hide_viewport
|
||||
if hide_viewport_status:
|
||||
obj.hide_viewport = False
|
||||
|
||||
# The 'Edge Split' modifier will disconnect faces from eachother, resulting in
|
||||
# a non-manifold mesh. Disable the edge split modifier from the modifier stack
|
||||
# before exporting. Original value will be set back at the end of this method
|
||||
edge_split_show_render_values = []
|
||||
edge_split_show_viewport_values = []
|
||||
for m in obj.modifiers:
|
||||
if m.type == 'EDGE_SPLIT':
|
||||
edge_split_show_render_values.append(m.show_render)
|
||||
edge_split_show_viewport_values.append(m.show_viewport)
|
||||
m.show_render = False
|
||||
m.show_viewport = False
|
||||
|
||||
triangulation_mod = obj.modifiers.new("flip_triangulate", "TRIANGULATE")
|
||||
triangulation_mod.quad_method = 'FIXED'
|
||||
|
||||
if is_b3d_28:
|
||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||
obj_eval = obj.evaluated_get(depsgraph)
|
||||
new_mesh = obj_eval.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||
else:
|
||||
new_mesh = obj.to_mesh(scene=bpy.context.scene,
|
||||
apply_modifiers=True,
|
||||
settings='RENDER')
|
||||
|
||||
vertex_components = []
|
||||
if matrix_world is None:
|
||||
for mv in new_mesh.vertices:
|
||||
v = mv.co
|
||||
vertex_components.append(v.x)
|
||||
vertex_components.append(v.y)
|
||||
vertex_components.append(v.z)
|
||||
else:
|
||||
if is_b3d_28:
|
||||
for mv in new_mesh.vertices:
|
||||
v = matrix_world @ mv.co
|
||||
vertex_components.append(v.x)
|
||||
vertex_components.append(v.y)
|
||||
vertex_components.append(v.z)
|
||||
else:
|
||||
for mv in new_mesh.vertices:
|
||||
v = matrix_world * mv.co
|
||||
vertex_components.append(v.x)
|
||||
vertex_components.append(v.y)
|
||||
vertex_components.append(v.z)
|
||||
|
||||
triangle_indices = []
|
||||
for t in new_mesh.polygons:
|
||||
for idx in t.vertices:
|
||||
triangle_indices.append(idx)
|
||||
|
||||
tmesh = TriangleMesh()
|
||||
tmesh.vertices = array.array('f', vertex_components)
|
||||
tmesh.triangles = array.array('i', triangle_indices)
|
||||
|
||||
if is_b3d_28:
|
||||
obj_eval.to_mesh_clear()
|
||||
else:
|
||||
new_mesh.user_clear()
|
||||
bpy.data.meshes.remove(new_mesh)
|
||||
|
||||
obj.modifiers.remove(triangulation_mod)
|
||||
|
||||
for m in obj.modifiers:
|
||||
if m.type == 'EDGE_SPLIT':
|
||||
m.show_render = edge_split_show_render_values.pop(0)
|
||||
m.show_viewport = edge_split_show_viewport_values.pop(0)
|
||||
|
||||
if is_b3d_28:
|
||||
if hide_viewport_status != obj.hide_viewport:
|
||||
obj.hide_viewport = hide_viewport_status
|
||||
|
||||
return tmesh
|
||||
|
||||
|
||||
def _set_mesh_smoothness(mesh_data, is_smooth):
|
||||
values = [is_smooth] * len(mesh_data.polygons)
|
||||
mesh_data.polygons.foreach_set("use_smooth", values)
|
||||
|
||||
|
||||
def _set_octane_mesh_type(obj, mesh_type):
|
||||
if hasattr(bpy.context.scene, 'octane') and mesh_type is not None:
|
||||
obj.octane.object_mesh_type = mesh_type
|
||||
|
||||
|
||||
def _transfer_mesh_materials(src_mesh_data, dst_mesh_data):
|
||||
material_names = []
|
||||
for m in src_mesh_data.materials:
|
||||
material_names.append(m.name)
|
||||
|
||||
for name in material_names:
|
||||
for m in bpy.data.materials:
|
||||
if m.name == name:
|
||||
dst_mesh_data.materials.append(m)
|
||||
break
|
||||
|
||||
|
||||
def swap_object_mesh_data_geometry(bl_object, vertices=[], triangles=[],
|
||||
mesh_name="Untitled",
|
||||
smooth_mesh=False,
|
||||
octane_mesh_type='Global'):
|
||||
if is_blender_281():
|
||||
# Vertex Groups (Blender >= 3.4)
|
||||
if is_blender_34():
|
||||
vg_layer_names = [vg.name for vg in bl_object.vertex_groups]
|
||||
active_vertex_layer_index = bl_object.vertex_groups.active_index
|
||||
|
||||
# UV Maps
|
||||
uv_layer_names = [uv.name for uv in bl_object.data.uv_layers]
|
||||
is_uv_layer_active = [uv.active for uv in bl_object.data.uv_layers]
|
||||
is_uv_layer_active_render = [uv.active_render for uv in bl_object.data.uv_layers]
|
||||
|
||||
# Color Attributes (Blender >= 3.2)
|
||||
if is_blender_32():
|
||||
ca_layer_names = [ca.name for ca in bl_object.data.color_attributes]
|
||||
ca_layer_data_types = [ca.data_type for ca in bl_object.data.color_attributes]
|
||||
ca_layer_domain_types = [ca.domain for ca in bl_object.data.color_attributes]
|
||||
active_color_layer_index = bl_object.data.color_attributes.active_color_index
|
||||
active_color_render_index = bl_object.data.color_attributes.render_color_index
|
||||
else:
|
||||
# Vertex Colors (Blender <= 3.1)
|
||||
vc_layer_names = [vc.name for vc in bl_object.data.vertex_colors]
|
||||
is_vc_layer_active = [vc.active for vc in bl_object.data.vertex_colors]
|
||||
is_vc_layer_active_render = [vc.active_render for vc in bl_object.data.vertex_colors]
|
||||
|
||||
vertices = numpy.array(vertices, dtype=numpy.float32)
|
||||
num_vertices = vertices.shape[0] // 3
|
||||
vertex_index = numpy.array(triangles, dtype=numpy.int32)
|
||||
loop_start = numpy.array(list(range(0, len(triangles), 3)), dtype=numpy.int32)
|
||||
num_loops = loop_start.shape[0]
|
||||
loop_total = numpy.array([3] * (len(triangles) // 3), dtype=numpy.int32)
|
||||
|
||||
bl_object.data.clear_geometry()
|
||||
bl_object.data.from_pydata(vertices, [], triangles)
|
||||
|
||||
_set_mesh_smoothness(bl_object.data, smooth_mesh)
|
||||
_set_octane_mesh_type(bl_object, octane_mesh_type)
|
||||
|
||||
# Vertex Groups (Blender >= 3.4)
|
||||
if is_blender_34():
|
||||
for i, name in enumerate(vg_layer_names):
|
||||
vg_layer = bl_object.vertex_groups.new(name=name)
|
||||
if active_vertex_layer_index >= 0:
|
||||
bl_object.vertex_groups.active_index = active_vertex_layer_index
|
||||
|
||||
# UV Maps
|
||||
for i, name in enumerate(uv_layer_names):
|
||||
uv_layer = bl_object.data.uv_layers.new(name=name)
|
||||
uv_layer.active = is_uv_layer_active[i]
|
||||
uv_layer.active_render = is_uv_layer_active_render[i]
|
||||
|
||||
# Color Attributes (Blender >= 3.2)
|
||||
if is_blender_32():
|
||||
for i, name in enumerate(ca_layer_names):
|
||||
ca_layer = bl_object.data.color_attributes.new(
|
||||
name=name,
|
||||
type=ca_layer_data_types[i],
|
||||
domain=ca_layer_domain_types[i]
|
||||
)
|
||||
# Unable to set active color/render index > 0. Possibly a Blender bug that we need
|
||||
# to report. For now, the Color Attributes layer will be limited to a single layer.
|
||||
# As far as we know, this feature does not need to be used outside of Octane Render.
|
||||
"""
|
||||
if active_color_layer_index >= 0:
|
||||
bl_object.data.color_attributes.active_color_index = active_color_layer_index
|
||||
if active_color_render_index >= 0:
|
||||
bl_object.data.color_attributes.render_color_index = active_color_render_index
|
||||
"""
|
||||
else:
|
||||
# Vertex Colors (Blender <= 3.1)
|
||||
for i, name in enumerate(vc_layer_names):
|
||||
vc_layer = bl_object.data.vertex_colors.new(name=name)
|
||||
vc_layer.active = is_vc_layer_active[i]
|
||||
vc_layer.active_render = is_vc_layer_active_render[i]
|
||||
|
||||
else:
|
||||
old_mesh_data = bl_object.data
|
||||
new_mesh_data = bpy.data.meshes.new(mesh_name)
|
||||
new_mesh_data.from_pydata(vertices, [], triangles)
|
||||
bl_object.data = new_mesh_data
|
||||
|
||||
_transfer_mesh_materials(old_mesh_data, new_mesh_data)
|
||||
_set_mesh_smoothness(new_mesh_data, smooth_mesh)
|
||||
_set_octane_mesh_type(bl_object, octane_mesh_type)
|
||||
|
||||
old_mesh_data.user_clear()
|
||||
bpy.data.meshes.remove(old_mesh_data)
|
||||
|
||||
|
||||
def get_addon_directory():
|
||||
this_filepath = os.path.dirname(os.path.realpath(__file__))
|
||||
addon_directory = os.path.dirname(this_filepath)
|
||||
return addon_directory
|
||||
|
||||
|
||||
def get_blender_preferences(context=None):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
if is_blender_28():
|
||||
return context.preferences
|
||||
else:
|
||||
return context.user_preferences
|
||||
|
||||
|
||||
def get_blender_preferences_temporary_directory(context=None):
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
return get_blender_preferences(context).filepaths.temporary_directory
|
||||
|
||||
|
||||
def get_addon_preferences(context=None):
|
||||
from ..properties import preferences_properties
|
||||
return preferences_properties.get_addon_preferences(context)
|
||||
|
||||
|
||||
#
|
||||
# UI Compatibility
|
||||
#
|
||||
|
||||
|
||||
def ui_split(ui_element, factor=None, align=None):
|
||||
if is_blender_28():
|
||||
if factor is None and align is None:
|
||||
return ui_element.split()
|
||||
elif factor is None:
|
||||
return ui_element.split(align=align)
|
||||
elif align is None:
|
||||
return ui_element.split(factor=factor)
|
||||
else:
|
||||
return ui_element.split(factor=factor, align=align)
|
||||
else:
|
||||
if factor is None and align is None:
|
||||
return ui_element.split()
|
||||
elif factor is None:
|
||||
return ui_element.split(align=align)
|
||||
elif align is None:
|
||||
return ui_element.split(percentage=factor)
|
||||
else:
|
||||
return ui_element.split(percentage=factor, align=align)
|
||||
|
||||
|
||||
def get_file_folder_icon():
|
||||
if is_blender_28():
|
||||
return "FILEBROWSER"
|
||||
else:
|
||||
return "FILESEL"
|
||||
|
||||
|
||||
def get_hide_off_icon():
|
||||
if is_blender_28():
|
||||
return "HIDE_OFF"
|
||||
else:
|
||||
return "RESTRICT_VIEW_OFF"
|
||||
|
||||
|
||||
def get_hide_on_icon():
|
||||
if is_blender_28():
|
||||
return "HIDE_ON"
|
||||
else:
|
||||
return "RESTRICT_VIEW_ON"
|
||||
|
||||
|
||||
def str_removesuffix(input_string, suffix):
|
||||
if suffix and input_string.endswith(suffix):
|
||||
return input_string[:-len(suffix)]
|
||||
return input_string
|
||||
Reference in New Issue
Block a user