2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -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