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,110 @@
# 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 = [
'error_operators',
'preferences_operators',
'object_operators',
'render_operators',
'cache_operators',
'world_operators',
'export_operators',
'material_operators',
'preset_operators',
'bake_operators',
'stats_operators',
'draw_grid_operators',
'draw_particles_operators',
'draw_force_field_operators',
'helper_operators',
'command_line_operators',
'support_operators',
'alembic_io_operators',
'add_flip_objects'
]
for module_name in reloadable_modules:
if module_name in locals():
importlib.reload(locals()[module_name])
import bpy
from . import (
error_operators,
preferences_operators,
object_operators,
render_operators,
cache_operators,
world_operators,
export_operators,
material_operators,
preset_operators,
bake_operators,
stats_operators,
draw_grid_operators,
draw_particles_operators,
draw_force_field_operators,
helper_operators,
command_line_operators,
support_operators,
alembic_io_operators,
add_flip_objects
)
def register():
error_operators.register()
preferences_operators.register()
object_operators.register()
render_operators.register()
cache_operators.register()
world_operators.register()
export_operators.register()
material_operators.register()
preset_operators.register()
bake_operators.register()
stats_operators.register()
draw_grid_operators.register()
draw_particles_operators.register()
draw_force_field_operators.register()
helper_operators.register()
command_line_operators.register()
support_operators.register()
alembic_io_operators.register()
add_flip_objects.register()
def unregister():
error_operators.unregister()
preferences_operators.unregister()
object_operators.unregister()
render_operators.unregister()
cache_operators.unregister()
world_operators.unregister()
export_operators.unregister()
material_operators.unregister()
preset_operators.unregister()
bake_operators.unregister()
stats_operators.unregister()
draw_grid_operators.unregister()
draw_particles_operators.unregister()
draw_force_field_operators.unregister()
helper_operators.unregister()
command_line_operators.unregister()
support_operators.unregister()
alembic_io_operators.unregister()
add_flip_objects.unregister()
@@ -0,0 +1,483 @@
# 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, mathutils, math
class VIEW3D_MT_FLIPFluidsAdd(bpy.types.Menu):
bl_idname = "VIEW3D_MT_FLIPFluidsAdd"
bl_label = "FLIP Fluids"
def draw(self, context):
column = self.layout.column(align=True)
column.operator("flip_fluid_operators.helper_create_domain", text="Create FLIP Domain", icon='CUBE')
column.operator(
"flip_fluid_operators.helper_add_objects",
text="FLIP Obstacle",
icon='MESH_CUBE'
).object_type="TYPE_OBSTACLE"
column.operator(
"flip_fluid_operators.helper_add_objects",
text="FLIP Fluid",
icon='MOD_FLUIDSIM'
).object_type="TYPE_FLUID"
column.operator(
"flip_fluid_operators.helper_add_objects",
text="FLIP Inflow",
icon='MOD_FLUIDSIM'
).object_type="TYPE_INFLOW"
column.operator(
"flip_fluid_operators.helper_add_objects",
text="FLIP Outflow",
icon='UGLYPACKAGE'
).object_type="TYPE_OUTFLOW"
column.operator(
"flip_fluid_operators.helper_add_objects",
text="FLIP Force Field",
icon='OUTLINER_OB_FORCE_FIELD'
).object_type="TYPE_FORCE_FIELD"
column.separator()
column.operator("flip_fluid_operators.add_quick_liquid", text="Create Quick Liquid", icon='MOD_FLUIDSIM')
column.operator("flip_fluid_operators.add_thick_viscous_liquid", text="Create Thick Viscous Liquid", icon='MOD_FLUIDSIM')
column.operator("flip_fluid_operators.add_thin_viscous_liquid", text="Create Thin Viscous Liquid", icon='MOD_FLUIDSIM')
column.separator()
column.operator("flip_fluid_operators.helper_remove_objects", text="Remove FLIP Object", icon='REMOVE')
column.operator("flip_fluid_operators.helper_delete_domain", text="Delete FLIP Domain", icon='X')
def add_menu_func(self, context):
self.layout.separator()
icon = context.scene.flip_fluid.get_logo_icon()
if icon is not None:
self.layout.menu("VIEW3D_MT_FLIPFluidsAdd", text="FLIP Fluids", icon_value=context.scene.flip_fluid.get_logo_icon().icon_id)
else:
self.layout.menu("VIEW3D_MT_FLIPFluidsAdd", text="FLIP Fluids", icon="MOD_FLUIDSIM")
def is_default_cube( bl_object):
if bl_object is None:
return False
if bl_object.type != 'MESH':
return False
if bl_object.name != "Cube":
return False
if len(bl_object.modifiers) != 0:
return False
mesh_data = bl_object.data
if mesh_data.name != "Cube":
return False
default_vertices = [
mathutils.Vector((-1.0, -1.0, -1.0)),
mathutils.Vector((-1.0, 1.0, -1.0)),
mathutils.Vector((1.0, 1.0, -1.0)),
mathutils.Vector((1.0, -1.0, -1.0)),
mathutils.Vector((-1.0, -1.0, 1.0)),
mathutils.Vector((-1.0, 1.0, 1.0)),
mathutils.Vector((1.0, 1.0, 1.0)),
mathutils.Vector((1.0, -1.0, 1.0))
]
default_faces = [
(4,5,1,0),
(5,6,2,1),
(6,7,3,2),
(7,4,0,3),
(0,1,2,3),
(7,6,5,4)
]
if len(mesh_data.vertices) != len(default_vertices) or len(mesh_data.polygons) != len(default_faces):
return False
mesh_vertices = sorted([v.co for v in mesh_data.vertices], key=lambda v: (v.x, v.y, v.z))
expected_mesh_vertices = sorted(default_vertices, key=lambda v: (v.x, v.y, v.z))
eps = 1e-5
for i in range(len(mesh_vertices)):
vdiff = mesh_vertices[i] - expected_mesh_vertices[i]
if abs(vdiff[0]) > eps or abs(vdiff[1]) > eps or abs(vdiff[2]) > eps:
return False
return True
class FlipFluidAddQuickLiquid(bpy.types.Operator):
bl_idname = "flip_fluid_operators.add_quick_liquid"
bl_label = "FLIP Fluids Quick Liquid"
bl_description = ("Create a basic FLIP Fluid liquid simulation. The Blend file must not already contain" +
" a domain object to run this operator. This operator will function best in a new" +
" saved Blend file with no FLIP objects")
@classmethod
def poll(cls, context):
return True
def check_and_report_operator_context_errors(self, context):
# Cannot set up simulation with an existing domain
bl_domain = bpy.context.scene.flip_fluid.get_domain_object()
if bl_domain is not None:
self.report({'ERROR'}, "Blend file already contains a domain <" + bl_domain.name + ">. Remove the domain or create and save a new Blend file.")
return {'CANCELLED'}
def create_quick_liquid(self):
if bpy.context.mode != 'OBJECT':
# Simulation setup must be in object mode
bpy.ops.object.mode_set(mode='OBJECT')
z_offset = 2.0
# Delete default cube if selected
bl_default_cube = bpy.context.active_object
if is_default_cube(bl_default_cube):
bpy.ops.object.select_all(action='DESELECT')
bl_default_cube.select_set(True)
bpy.ops.object.delete()
# Create Domain
bpy.ops.mesh.primitive_cube_add(size=4.0, location=(0.0, 0.0, 0.0 + z_offset))
bl_cube = bpy.context.active_object
bl_cube.name = "FLIP Domain"
bpy.ops.flip_fluid_operators.flip_fluid_add()
bl_cube.flip_fluid.object_type = 'TYPE_DOMAIN'
# Create Fluid Cube
bpy.ops.mesh.primitive_cube_add(size=4.0, location=(0.0, 0.0, -1.75 + z_offset), scale=(1.0, 1.0, 0.125))
bl_cube = bpy.context.active_object
bl_cube.name = "Fluid"
bpy.ops.flip_fluid_operators.flip_fluid_add()
bl_cube.flip_fluid.object_type = 'TYPE_FLUID'
# Create Inflow
bpy.ops.mesh.primitive_cylinder_add(radius=0.25, location=(0.0, 0.0, 1.25 + z_offset), scale=(1.0, 1.0, 0.25))
bl_cylinder = bpy.context.active_object
bl_cylinder.name = "Inflow"
bpy.ops.flip_fluid_operators.flip_fluid_add()
bl_cylinder.flip_fluid.object_type = 'TYPE_INFLOW'
inflow_props = bl_cylinder.flip_fluid.get_property_group()
inflow_props.inflow_velocity = (0.0, 0.0, -3.0)
inflow_props.is_enabled = True
bl_cylinder.keyframe_insert(data_path="flip_fluid.inflow.is_enabled", frame=1)
inflow_props.is_enabled = False
bl_cylinder.keyframe_insert(data_path="flip_fluid.inflow.is_enabled", frame=20)
# Create Obstacle
bpy.ops.mesh.primitive_ico_sphere_add(
radius=0.75,
location=(0.0, 0.0, -0.25 + z_offset),
scale=(1.0, 1.0, 0.25),
rotation=(0.0, 0.5, 0.0),
subdivisions=4
)
bpy.ops.object.shade_smooth()
bl_sphere = bpy.context.active_object
bl_sphere.name = "Obstacle"
bpy.ops.flip_fluid_operators.flip_fluid_add()
bl_sphere.flip_fluid.object_type = 'TYPE_OBSTACLE'
# Domain Settings
domain_properties = bpy.context.scene.flip_fluid.get_domain_properties()
domain_properties.materials.surface_material = 'FF Water (ocean volumetric)'
# Surface Settings
domain_properties = bpy.context.scene.flip_fluid.get_domain_properties()
bl_fluid_surface = domain_properties.mesh_cache.surface.get_cache_object()
for mod in bl_fluid_surface.modifiers:
if mod.type == 'SMOOTH':
mod.factor = 1.5
mod.iterations = 5
# Misc Settings
bpy.ops.flip_fluid_operators.helper_initialize_motion_blur()
bpy.ops.flip_fluid_operators.helper_organize_outliner()
if bpy.data.filepath:
bpy.context.scene.render.filepath = ""
bpy.ops.flip_fluid_operators.relative_to_blend_render_output()
def execute(self, context):
error_return = self.check_and_report_operator_context_errors(context)
if error_return:
return error_return
self.create_quick_liquid()
return {'FINISHED'}
class FlipFluidAddThickViscousLiquid(bpy.types.Operator):
bl_idname = "flip_fluid_operators.add_thick_viscous_liquid"
bl_label = "FLIP Fluids Thick Viscous Liquid"
bl_description = ("Create a basic thick viscous liquid that buckles and coils. The Blend file must not already contain" +
" a domain object to run this operator. This operator will function best in a new" +
" saved Blend file with no FLIP objects")
@classmethod
def poll(cls, context):
return True
def check_and_report_operator_context_errors(self, context):
# Cannot set up simulation with an existing domain
bl_domain = bpy.context.scene.flip_fluid.get_domain_object()
if bl_domain is not None:
self.report({'ERROR'}, "Blend file already contains a domain <" + bl_domain.name + ">. Remove the domain or create and save a new Blend file.")
return {'CANCELLED'}
def create_thick_viscous_liquid(self):
if bpy.context.mode != 'OBJECT':
# Simulation setup must be in object mode
bpy.ops.object.mode_set(mode='OBJECT')
z_offset = 2.0
# Delete default cube if selected
bl_default_cube = bpy.context.active_object
if is_default_cube(bl_default_cube):
bpy.ops.object.select_all(action='DESELECT')
bl_default_cube.select_set(True)
bpy.ops.object.delete()
# Create Domain
bpy.ops.mesh.primitive_cube_add(size=4.0, location=(0.0, 0.0, 0.0 + z_offset), scale=(0.5, 0.5, 1.0))
bl_domain = bpy.context.active_object
bl_domain.name = "FLIP Domain"
bpy.ops.flip_fluid_operators.flip_fluid_add()
bl_domain.flip_fluid.object_type = 'TYPE_DOMAIN'
# Domain Settings
domain_props = bl_domain.flip_fluid.get_property_group()
domain_props.simulation.resolution = 120
domain_props.surface.subdivisions = 2
domain_props.world.enable_viscosity = True
domain_props.world.viscosity = 15
domain_props.materials.surface_material = 'FF Caramel'
domain_props.advanced.jitter_surface_particles = True
# Create Inflow
bpy.ops.mesh.primitive_cylinder_add(radius=0.25, location=(-0.125, -0.125, 1.25 + z_offset), scale=(0.4, 0.4, 0.25))
bl_cylinder = bpy.context.active_object
bl_cylinder.name = "Inflow"
bpy.ops.flip_fluid_operators.flip_fluid_add()
bl_cylinder.flip_fluid.object_type = 'TYPE_INFLOW'
inflow_props = bl_cylinder.flip_fluid.get_property_group()
inflow_props.is_enabled = True
bl_cylinder.keyframe_insert(data_path="flip_fluid.inflow.is_enabled", frame=1)
inflow_props.is_enabled = False
bl_cylinder.keyframe_insert(data_path="flip_fluid.inflow.is_enabled", frame=150)
# Create Obstacle
scale = math.sqrt(5.0/4.0)
bpy.ops.mesh.primitive_ico_sphere_add(
radius=2.0,
location=(-1.0, -1.0, -3.0 + z_offset),
scale=(scale, scale, scale),
subdivisions=5
)
bl_sphere = bpy.context.active_object
bl_sphere.name = "Obstacle"
bpy.ops.flip_fluid_operators.flip_fluid_add()
bl_sphere.flip_fluid.object_type = 'TYPE_OBSTACLE'
obstacle_props = bl_sphere.flip_fluid.get_property_group()
obstacle_props.friction = 1.0
mod = bl_sphere.modifiers.new("Boolean", "BOOLEAN")
mod.operation = 'INTERSECT'
mod.object = bl_domain
# Surface Settings
domain_properties = bpy.context.scene.flip_fluid.get_domain_properties()
bl_fluid_surface = domain_properties.mesh_cache.surface.get_cache_object()
for mod in bl_fluid_surface.modifiers:
if mod.type == 'SMOOTH':
mod.factor = 1.5
mod.iterations = 40
# Misc Settings
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 250
bpy.context.scene.render.fps = 24
bpy.ops.flip_fluid_operators.helper_initialize_motion_blur()
bpy.ops.flip_fluid_operators.helper_organize_outliner()
if bpy.data.filepath:
bpy.context.scene.render.filepath = ""
bpy.ops.flip_fluid_operators.relative_to_blend_render_output()
def execute(self, context):
error_return = self.check_and_report_operator_context_errors(context)
if error_return:
return error_return
self.create_thick_viscous_liquid()
return {'FINISHED'}
class FlipFluidAddThinViscousLiquid(bpy.types.Operator):
bl_idname = "flip_fluid_operators.add_thin_viscous_liquid"
bl_label = "FLIP Fluids Thin Viscous Liquid"
bl_description = ("Create a smooth paint-like liquid with low viscosity and surface tension. The Blend file must not already contain" +
" a domain object to run this operator. This operator will function best in a new" +
" saved Blend file with no FLIP objects")
@classmethod
def poll(cls, context):
return True
def check_and_report_operator_context_errors(self, context):
# Cannot set up simulation with an existing domain
bl_domain = bpy.context.scene.flip_fluid.get_domain_object()
if bl_domain is not None:
self.report({'ERROR'}, "Blend file already contains a domain <" + bl_domain.name + ">. Remove the domain or create and save a new Blend file.")
return {'CANCELLED'}
def create_thin_viscous_liquid(self):
if bpy.context.mode != 'OBJECT':
# Simulation setup must be in object mode
bpy.ops.object.mode_set(mode='OBJECT')
z_offset = 2.0
# Delete default cube if selected
bl_default_cube = bpy.context.active_object
if is_default_cube(bl_default_cube):
bpy.ops.object.select_all(action='DESELECT')
bl_default_cube.select_set(True)
bpy.ops.object.delete()
# Create Domain
bpy.ops.mesh.primitive_cube_add(size=4.0, location=(0.0, 0.0, -0.8 + z_offset), scale=(0.5, 0.5, 0.6))
bl_domain = bpy.context.active_object
bl_domain.name = "FLIP Domain"
bpy.ops.flip_fluid_operators.flip_fluid_add()
bl_domain.flip_fluid.object_type = 'TYPE_DOMAIN'
# Domain Settings
domain_props = bl_domain.flip_fluid.get_property_group()
domain_props.simulation.resolution = 100
domain_props.world.enable_viscosity = True
domain_props.world.viscosity_settings_expaned = True
domain_props.world.viscosity = 0.01
domain_props.world.surface_tension_settings_expaned = True
domain_props.world.enable_surface_tension = True
domain_props.world.surface_tension = 0.3
domain_props.materials.surface_material = 'FF Caramel'
domain_props.advanced.jitter_surface_particles = True
# Create Inflow
bpy.ops.mesh.primitive_cylinder_add(radius=0.25, location=(-0.5, -0.5, 0.0 + z_offset), scale=(0.4, 0.4, 0.125))
bl_cylinder = bpy.context.active_object
bl_cylinder.name = "Inflow"
bpy.ops.flip_fluid_operators.flip_fluid_add()
bl_cylinder.flip_fluid.object_type = 'TYPE_INFLOW'
# Create Obstacle
scale = math.sqrt(5.0/4.0)
bpy.ops.mesh.primitive_ico_sphere_add(
radius=2.0,
location=(-1.0, -1.0, -3.0 + z_offset),
scale=(scale, scale, scale),
subdivisions=5
)
bl_sphere = bpy.context.active_object
bl_sphere.name = "Obstacle"
bpy.ops.flip_fluid_operators.flip_fluid_add()
bl_sphere.flip_fluid.object_type = 'TYPE_OBSTACLE'
obstacle_props = bl_sphere.flip_fluid.get_property_group()
obstacle_props.friction = 1.0
mod = bl_sphere.modifiers.new("Boolean", "BOOLEAN")
mod.operation = 'INTERSECT'
mod.object = bl_domain
# Surface Settings
domain_properties = bpy.context.scene.flip_fluid.get_domain_properties()
bl_fluid_surface = domain_properties.mesh_cache.surface.get_cache_object()
for mod in bl_fluid_surface.modifiers:
if mod.type == 'SMOOTH':
mod.factor = 1.5
mod.iterations = 15
# Misc Settings
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 200
bpy.context.scene.render.fps = 24
bpy.ops.flip_fluid_operators.helper_initialize_motion_blur()
bpy.ops.flip_fluid_operators.helper_organize_outliner()
if bpy.data.filepath:
bpy.context.scene.render.filepath = ""
bpy.ops.flip_fluid_operators.relative_to_blend_render_output()
def execute(self, context):
error_return = self.check_and_report_operator_context_errors(context)
if error_return:
return error_return
self.create_thin_viscous_liquid()
return {'FINISHED'}
def register():
bpy.utils.register_class(FlipFluidAddQuickLiquid)
bpy.utils.register_class(FlipFluidAddThickViscousLiquid)
bpy.utils.register_class(FlipFluidAddThinViscousLiquid)
bpy.utils.register_class(VIEW3D_MT_FLIPFluidsAdd)
bpy.types.VIEW3D_MT_add.append(add_menu_func)
def unregister():
bpy.utils.unregister_class(FlipFluidAddQuickLiquid)
bpy.utils.unregister_class(FlipFluidAddThickViscousLiquid)
bpy.utils.unregister_class(FlipFluidAddThinViscousLiquid)
bpy.utils.unregister_class(VIEW3D_MT_FLIPFluidsAdd)
bpy.types.VIEW3D_MT_add.remove(add_menu_func)
@@ -0,0 +1,256 @@
# 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, bpy_extras
import os
from . import helper_operators
class FLIPFluidsAlembicImporter(bpy.types.Operator, bpy_extras.io_utils.ImportHelper):
bl_idname = "flip_fluid_operators.flip_fluids_alembic_importer"
bl_label = "Import FF Alembic"
bl_options = {'PRESET', 'UNDO'}
bl_description = ("Import and set up a FLIP Fluids Alembic cache")
filename_ext = ".abc"
filter_glob: bpy.props.StringProperty(default="*.abc", options={'HIDDEN'})
def find_flip_fluids_mesh(self, bl_object_list, name_prefix):
bl_object = None
for obj in bl_object_list:
if obj.type == 'MESH' and obj.name.startswith(name_prefix):
bl_object = obj
break
return bl_object
def apply_default_modifier_settings(self, target_object, gn_modifier):
gn_modifier["Input_2_use_attribute"] = True
gn_modifier["Input_2_attribute_name"] = 'flip_velocity'
gn_modifier["Output_3_attribute_name"] = 'velocity'
gn_name = gn_modifier.name
if gn_name.startswith("FF_GeometryNodesSurface"):
# Depending on FLIP Fluids version, the GN set up may not
# have these inputs. Available in FLIP Fluids 1.7.2 or later.
try:
# Enable Motion Blur
gn_modifier["Input_6"] = True
except:
pass
if gn_name.startswith("FF_GeometryNodesWhitewater") or gn_name.startswith("FF_GeometryNodesFluidParticles"):
# Depending on FLIP Fluids version, the GN set up may not
# have these inputs. Available in FLIP Fluids 1.7.2 or later.
try:
# Material
gn_modifier["Input_5"] = target_object.active_material
except:
pass
try:
# Enable Motion Blur
gn_modifier["Input_8"] = True
except:
pass
try:
# Enable Point Cloud
gn_modifier["Input_9"] = True
except:
pass
try:
# Enable Instancing
gn_modifier["Input_10"] = False
except:
pass
def add_smooth_modifier(self, target_object):
smooth_mod = target_object.modifiers.new("FF_Smooth", "SMOOTH")
smooth_mod.factor = 1.5
smooth_mod.iterations = 0
return smooth_mod
def execute(self, context):
print("FLIP Fluids Alembic Import: <" + self.filepath + ">")
bpy.ops.wm.alembic_import(filepath=self.filepath)
bl_fluid_surface = self.find_flip_fluids_mesh(bpy.context.selected_objects, "fluid_surface")
bl_fluid_particles = self.find_flip_fluids_mesh(bpy.context.selected_objects, "fluid_particles")
bl_whitewater_foam = self.find_flip_fluids_mesh(bpy.context.selected_objects, "whitewater_foam")
bl_whitewater_bubble = self.find_flip_fluids_mesh(bpy.context.selected_objects, "whitewater_bubble")
bl_whitewater_spray = self.find_flip_fluids_mesh(bpy.context.selected_objects, "whitewater_spray")
bl_whitewater_dust = self.find_flip_fluids_mesh(bpy.context.selected_objects, "whitewater_dust")
blend_filename = "geometry_nodes_library.blend"
parent_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
resource_filepath = os.path.join(parent_path, "resources", "geometry_nodes", blend_filename)
found_mesh_cache_list = []
if bl_fluid_surface is not None:
self.add_smooth_modifier(bl_fluid_surface)
helper_operators.add_geometry_node_modifier(bl_fluid_surface, resource_filepath, "FF_AlembicImportSurface")
gn_modifier = helper_operators.add_geometry_node_modifier(bl_fluid_surface, resource_filepath, "FF_GeometryNodesSurface")
self.apply_default_modifier_settings(bl_fluid_surface, gn_modifier)
self.report({'INFO'}, "Found fluid surface cache... Initialized geometry nodes")
found_mesh_cache_list.append("Surface")
if bl_fluid_particles is not None:
helper_operators.add_geometry_node_modifier(bl_fluid_particles, resource_filepath, "FF_AlembicImportFluidParticles")
gn_modifier = helper_operators.add_geometry_node_modifier(bl_fluid_particles, resource_filepath, "FF_GeometryNodesFluidParticles")
self.apply_default_modifier_settings(bl_fluid_particles, gn_modifier)
self.report({'INFO'}, "Found fluid particle cache... Initialized geometry nodes")
found_mesh_cache_list.append("FluidParticles")
if bl_whitewater_foam is not None:
helper_operators.add_geometry_node_modifier(bl_whitewater_foam, resource_filepath, "FF_AlembicImportWhitewaterFoam")
gn_modifier = helper_operators.add_geometry_node_modifier(bl_whitewater_foam, resource_filepath, "FF_GeometryNodesWhitewaterFoam")
self.apply_default_modifier_settings(bl_whitewater_foam, gn_modifier)
self.report({'INFO'}, "Found whitewater foam cache... Initialized geometry nodes")
found_mesh_cache_list.append("Foam")
if bl_whitewater_bubble is not None:
helper_operators.add_geometry_node_modifier(bl_whitewater_bubble, resource_filepath, "FF_AlembicImportWhitewaterBubble")
gn_modifier = helper_operators.add_geometry_node_modifier(bl_whitewater_bubble, resource_filepath, "FF_GeometryNodesWhitewaterBubble")
self.apply_default_modifier_settings(bl_whitewater_bubble, gn_modifier)
self.report({'INFO'}, "Found whitewater bubble cache... Initialized geometry nodes")
found_mesh_cache_list.append("Bubble")
if bl_whitewater_spray is not None:
helper_operators.add_geometry_node_modifier(bl_whitewater_spray, resource_filepath, "FF_AlembicImportWhitewaterSpray")
gn_modifier = helper_operators.add_geometry_node_modifier(bl_whitewater_spray, resource_filepath, "FF_GeometryNodesWhitewaterSpray")
self.apply_default_modifier_settings(bl_whitewater_spray, gn_modifier)
self.report({'INFO'}, "Found whitewater spray cache... Initialized geometry nodes")
found_mesh_cache_list.append("Spray")
if bl_whitewater_dust is not None:
helper_operators.add_geometry_node_modifier(bl_whitewater_dust, resource_filepath, "FF_AlembicImportWhitewaterDust")
gn_modifier = helper_operators.add_geometry_node_modifier(bl_whitewater_dust, resource_filepath, "FF_GeometryNodesWhitewaterDust")
self.apply_default_modifier_settings(bl_whitewater_dust, gn_modifier)
self.report({'INFO'}, "Found whitewater dust cache... Initialized geometry nodes")
found_mesh_cache_list.append("Dust")
if found_mesh_cache_list:
found_mesh_cache_string = "/".join(found_mesh_cache_list)
self.report({'INFO'}, "Found and initialized " + found_mesh_cache_string + " objects and geometry nodes.")
else:
self.report({'WARNING'}, "No valid FLIP Fluids addon meshes found. Is this Alembic file a FLIP Fluids addon export?")
return {'FINISHED'}
class FLIPFluidsAlembicExporter(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
bl_idname = "flip_fluid_operators.flip_fluids_alembic_exporter"
bl_label = "Export FF Alembic"
bl_options = {'PRESET', 'UNDO'}
bl_description = ("Prepare a FLIP Fluids simulation for Alembic export. After the file dialog, this exporter will" +
" launch a new command line window and start exporting the simulation to the" +
" Alembic (.abc) format. This Blend file will need to be saved before accessing"
" this operator")
filename_ext = ".abc"
filter_glob: bpy.props.StringProperty(default="*.abc", options={'HIDDEN'})
@classmethod
def poll(cls, context):
return context.scene.flip_fluid.get_domain_object() is not None and bool(bpy.data.filepath)
def draw(self, context):
hprops = context.scene.flip_fluid_helper
self.layout.use_property_split = True
self.layout.use_property_decorate = False
header, body = self.layout.panel("alembic_scene", default_closed=False)
header.label(text="Scene")
if body:
column = body.column(align=True)
column.prop(hprops, "alembic_frame_range_mode", text="Frame Range")
if hprops.alembic_frame_range_mode == 'FRAME_RANGE_TIMELINE':
column.prop(context.scene, "frame_start")
column.prop(context.scene, "frame_end")
else:
column.prop(hprops.alembic_frame_range_custom, "value_min")
column.prop(hprops.alembic_frame_range_custom, "value_max")
column.separator()
column.prop(hprops, "alembic_global_scale")
header, body = self.layout.panel("alembic_include", default_closed=False)
header.label(text="Include")
if body:
column = body.column(heading="Mesh", align=True)
column.prop(hprops, "alembic_export_surface")
column.prop(hprops, "alembic_export_fluid_particles")
column.prop(hprops, "alembic_export_foam")
column.prop(hprops, "alembic_export_bubble")
column.prop(hprops, "alembic_export_spray")
column.prop(hprops, "alembic_export_dust")
column = body.column(heading="Attributes", align=True)
column.prop(hprops, "alembic_export_velocity")
column.prop(hprops, "alembic_export_color")
header, body = self.layout.panel("alembic_command", default_closed=True)
header.label(text="Command")
if body:
column = body.column(heading="Attributes", align=True)
column.operator("flip_fluid_operators.helper_cmd_alembic_export_to_clipboard", text="Copy Command to Clipboard", icon='COPYDOWN')
def execute(self, context):
print("FLIP Fluids Alembic Export: <" + self.filepath + ">")
hprops = context.scene.flip_fluid_helper
hprops.alembic_output_filepath = self.filepath
bpy.ops.flip_fluid_operators.helper_command_line_alembic_export('INVOKE_DEFAULT')
return {'FINISHED'}
def menu_func_import(self, context):
self.layout.operator(FLIPFluidsAlembicImporter.bl_idname, text="FLIP Fluids Alembic (.abc)")
def menu_func_export(self, context):
self.layout.operator(FLIPFluidsAlembicExporter.bl_idname, text="FLIP Fluids Alembic (.abc)")
def register():
bpy.utils.register_class(FLIPFluidsAlembicImporter)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
bpy.utils.register_class(FLIPFluidsAlembicExporter)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
bpy.utils.unregister_class(FLIPFluidsAlembicImporter)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
bpy.utils.unregister_class(FLIPFluidsAlembicExporter)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
@@ -0,0 +1,670 @@
# 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, json, threading
from .. import bake
from ..objects import flip_fluid_geometry_exporter
from .. import export
from ..utils import installation_utils
from ..utils import audio_utils
from ..filesystem import filesystem_protection_layer as fpl
from ..utils import version_compatibility_utils as vcu
_IS_BAKE_OPERATOR_RUNNING = False
_IS_CMD_BAKE_OPERATOR_RUNNING = False
def _notify_bake_operator_running():
global _IS_BAKE_OPERATOR_RUNNING
_IS_BAKE_OPERATOR_RUNNING = True
def _notify_bake_operator_cancelled():
global _IS_BAKE_OPERATOR_RUNNING
_IS_BAKE_OPERATOR_RUNNING = False
def is_bake_operator_running():
global _IS_BAKE_OPERATOR_RUNNING
return _IS_BAKE_OPERATOR_RUNNING
def _notify_cmd_bake_operator_running():
global _IS_CMD_BAKE_OPERATOR_RUNNING
_IS_CMD_BAKE_OPERATOR_RUNNING = True
def _notify_cmd_bake_operator_cancelled():
global _IS_CMD_BAKE_OPERATOR_RUNNING
_IS_CMD_BAKE_OPERATOR_RUNNING = False
def is_bake_cmd_operator_running():
global _IS_CMD_BAKE_OPERATOR_RUNNING
return _IS_CMD_BAKE_OPERATOR_RUNNING
def update_stats(context=None):
if context is None:
context = bpy.context
num_updated_frames = 0
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
cache_dir = dprops.cache.get_cache_abspath()
statsfilepath = os.path.join(cache_dir, dprops.stats.stats_filename)
if not os.path.isfile(statsfilepath):
try:
# Case that the cache directory path is not valid
with open(statsfilepath, 'w', encoding='utf-8') as f:
f.write(json.dumps({}, sort_keys=True, indent=4))
except:
return num_updated_frames
temp_dir = os.path.join(cache_dir, "temp")
if not os.path.isdir(temp_dir):
return num_updated_frames
stat_prefix = "framestats"
stat_extension = ".data"
stat_files = [f for f in os.listdir(temp_dir) if os.path.isfile(os.path.join(temp_dir, f))]
stat_files = [f for f in stat_files if f.endswith(stat_extension)]
stat_files = [f for f in stat_files if (f[len(stat_prefix):-len(stat_extension)]).isdigit()]
stat_files = [os.path.join(temp_dir, f) for f in stat_files if f.endswith(stat_extension)]
if not stat_files:
return num_updated_frames
with open(statsfilepath, 'r', encoding='utf-8') as f:
stats_dict = json.loads(f.read())
for statpath in stat_files:
filename = os.path.basename(statpath)
frameno = int(filename[len(stat_prefix):-len(stat_extension)])
with open(statpath, 'r', encoding='utf-8') as frame_stats:
try:
frame_stats_dict = json.loads(frame_stats.read())
except:
# stats data may not be finished writing which could
# result in a decode error. Skip this data for now and
# process the next time stats are updated.
continue
stats_dict[str(frameno)] = frame_stats_dict
fpl.delete_file(statpath, error_ok=True)
num_updated_frames += 1
with open(statsfilepath, 'w', encoding='utf-8') as f:
f.write(json.dumps(stats_dict, sort_keys=True, indent=4))
dprops.stats.is_stats_current = False
context.scene.flip_fluid_helper.frame_complete_callback()
dprops.bake.frame_complete_callback()
return num_updated_frames
class BakeData(object):
def __init__(self):
self.reset()
def reset(self):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
self.progress = 0.0
self.completed_frames = 0
self.is_finished = False
self.is_initialized = False
self.is_cancelled = False
self.is_safe_to_exit = True
self.is_console_output_enabled = True
if dprops is not None:
self.is_console_output_enabled = dprops.debug.display_console_output
self.error_message = ""
class BakeFluidSimulation(bpy.types.Operator):
bl_idname = "flip_fluid_operators.bake_fluid_simulation"
bl_label = "Bake Fluid Simulation"
bl_description = "Run fluid simulation"
bl_options = {'REGISTER'}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.timer = None
self.thread = None
self.is_export_operator_launched = False
self.is_thread_launched = False
self.is_thread_finished = False
self.is_updating_status = False
self.data = BakeData()
def _get_domain_properties(self):
return bpy.context.scene.flip_fluid.get_domain_properties()
def _reset_bake(self, context):
dprops = self._get_domain_properties()
dprops.bake.is_simulation_running = True
dprops.bake.bake_progress = 0.0
dprops.bake.num_baked_frames = 0
dprops.stats.refresh_stats()
self.data.reset()
def _initialize_domain_properties_frame_range(self, context):
dprops = self._get_domain_properties()
frame_start, frame_end = dprops.simulation.get_frame_range()
dprops.simulation.frame_start = frame_start
dprops.simulation.frame_end = frame_end
def _initialize_domain(self, context):
dprops = self._get_domain_properties()
self._initialize_domain_properties_frame_range(context)
dprops.mesh_cache.reset_cache_objects()
def _get_export_filepath(self):
dprops = self._get_domain_properties()
return os.path.join(dprops.cache.get_cache_abspath(),
dprops.bake.export_directory_name,
dprops.bake.export_filename)
def _launch_thread(self):
dprops = self._get_domain_properties()
savestate_id = dprops.simulation.get_selected_savestate_id()
cache_directory = dprops.cache.get_cache_abspath()
dprops.bake.export_filepath = self._get_export_filepath()
self.data.progress = 0.0
self.thread = threading.Thread(
target=bake.bake,
args=(dprops.bake.export_filepath, cache_directory, self.data, savestate_id,),
daemon=True
)
self.thread.start()
def _update_stats(self, context=None):
update_stats(context)
def _update_status(self, context):
if self.thread and not self.thread.is_alive():
self.is_thread_finished = True
self.thread = None
if not self.data.is_cancelled:
is_error = bool(self.data.error_message)
self.simulation_ended_handler(context, is_error)
if self.data.error_message:
bpy.ops.flip_fluid_operators.display_error(
'INVOKE_DEFAULT',
error_message="Error Baking Fluid Simulation",
error_description=self.data.error_message,
popup_width=400
)
self._update_stats(context)
dprops = self._get_domain_properties()
dprops.bake.is_bake_initialized = self.data.is_initialized
dprops.bake.bake_progress = self.data.progress
dprops.bake.num_baked_frames = self.data.completed_frames
dprops.bake.is_safe_to_exit = self.data.is_safe_to_exit
self.data.is_cancelled = dprops.bake.is_bake_cancelled
try:
# Depending on window, area may be None
context.area.tag_redraw()
except:
pass
def _cancel_bake(self, context):
if self.is_thread_finished:
return
dprops = self._get_domain_properties()
dprops.bake.is_bake_cancelled = True
self._update_status(context)
@classmethod
def poll(cls, context):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return not dprops.bake.is_simulation_running
def modal(self, context, event):
dprops = self._get_domain_properties()
is_exporting = (not dprops.bake.is_autosave_available or
dprops.simulation.update_settings_on_resume)
if not self.is_thread_launched and not self.is_export_operator_launched and is_exporting:
bpy.ops.flip_fluid_operators.export_fluid_simulation("INVOKE_DEFAULT")
self.is_export_operator_launched = True
if dprops.bake.is_export_operator_running and is_exporting:
return {'PASS_THROUGH'}
if not self.is_thread_launched and dprops.bake.is_bake_cancelled:
self.cancel(context)
return {'FINISHED'}
if not self.is_thread_launched:
self._launch_thread()
self.is_thread_launched = True
if event.type == 'TIMER' and not self.is_updating_status:
self.is_updating_status = True
self._update_status(context)
self.is_updating_status = False
if self.is_thread_finished:
self.cancel(context)
return {'FINISHED'}
return {'PASS_THROUGH'}
# Called when simulation ends either successfully or through an error
def simulation_ended_handler(self, context, is_error):
if vcu.get_addon_preferences().enable_bake_alarm:
json_filepath = os.path.join(audio_utils.get_sounds_directory(), "alarm", "sound_data.json")
audio_utils.play_sound(json_filepath)
def execute(self, context):
if not installation_utils.is_installation_complete():
self.report({"ERROR"},
"FLIP Fluids installation incomplete. Restart Blender to complete installation. If you think this is an error, please contact the developers.")
self.cancel(context)
return {'CANCELLED'}
if not context.scene.flip_fluid.is_domain_object_set():
self.report({"ERROR_INVALID_INPUT"},
"Fluid simulation requires a domain object")
self.cancel(context)
return {'CANCELLED'}
if context.scene.flip_fluid.get_num_domain_objects() > 1:
self.report({"ERROR_INVALID_INPUT"},
"There must be only one domain object in the Blend file")
self.cancel(context)
return {'CANCELLED'}
if not context.scene.flip_fluid.is_domain_in_active_scene():
self.report({"ERROR"},
"Active scene must contain domain object to begin baking. Select the scene that contains the domain object and try again.")
self.cancel(context)
return {'CANCELLED'}
dprops = self._get_domain_properties()
if dprops.bake.is_simulation_running:
self.cancel(context)
return {'CANCELLED'}
cache_directory = dprops.cache.get_cache_abspath()
if not os.path.exists(cache_directory):
try:
os.makedirs(cache_directory)
except:
msg = "Unable to create cache directory: <" + cache_directory + "> "
msg += "Set the cache directory in the 'Domain > FLIP Fluid Cache panel' to a location on your system with write permissions."
self.report({"ERROR_INVALID_INPUT"}, msg)
return {'CANCELLED'}
dprops.cache.mark_cache_directory_set()
self._reset_bake(context)
self._initialize_domain(context)
context.window_manager.modal_handler_add(self)
self.timer = context.window_manager.event_timer_add(0.1, window=context.window)
_notify_bake_operator_running()
return {'RUNNING_MODAL'}
def cancel(self, context):
if self.timer:
context.window_manager.event_timer_remove(self.timer)
self.timer = None
dprops = self._get_domain_properties()
if dprops is None:
return
dprops.bake.is_simulation_running = False
dprops.bake.is_bake_cancelled = False
dprops.bake.check_autosave()
try:
# Depending on window, area may be None
context.area.tag_redraw()
except:
pass
_notify_bake_operator_cancelled()
class BakeFluidSimulationCommandLine(bpy.types.Operator):
bl_idname = "flip_fluid_operators.bake_fluid_simulation_cmd"
bl_label = "Bake Fluid Simulation"
bl_description = "Bake fluid simulation from command line"
bl_options = {'REGISTER'}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.thread = None
self.mesh_data = {}
self.data = BakeData()
self.geometry_exporter = None
def _get_domain_properties(self):
return bpy.context.scene.flip_fluid.get_domain_properties()
def _reset_bake(self, context):
dprops = self._get_domain_properties()
dprops.bake.is_simulation_running = True
dprops.bake.bake_progress = 0.0
dprops.bake.num_baked_frames = 0
dprops.stats.refresh_stats()
self.data.reset()
def _initialize_domain_properties_frame_range(self, context):
dprops = self._get_domain_properties()
frame_start, frame_end = dprops.simulation.get_frame_range()
dprops.simulation.frame_start = frame_start
dprops.simulation.frame_end = frame_end
def _initialize_domain(self, context):
dprops = self._get_domain_properties()
self._initialize_domain_properties_frame_range(context)
dprops.mesh_cache.reset_cache_objects()
def _get_export_directory(self):
dprops = self._get_domain_properties()
return os.path.join(dprops.cache.get_cache_abspath(),
dprops.bake.export_directory_name)
def _initialize_geometry_exporter(self, context):
print("Exporting Simulation Meshes:")
print("------------------------------------------------------------")
export_dir = self._get_export_directory()
self.geometry_exporter = flip_fluid_geometry_exporter.GeometryExportManager(export_dir)
export.add_objects_to_geometry_exporter(self.geometry_exporter)
def _get_logfile_name(self, context):
dprops = self._get_domain_properties()
cache_directory = dprops.cache.get_cache_abspath()
logs_directory = os.path.join(cache_directory, "logs")
basename = os.path.basename(bpy.data.filepath)
basename = os.path.splitext(basename)[0]
if not basename:
basename = "untitled"
filename = basename
filepath = os.path.join(logs_directory, filename + ".txt")
if os.path.isfile(filepath):
for i in range(1, 1000):
filename = basename + "." + str(i).zfill(3)
filepath = os.path.join(logs_directory, filename + ".txt")
if not os.path.isfile(filepath):
break;
return filename + ".txt"
def _initialize_export_operator(self, context):
dprops = self._get_domain_properties()
dprops.bake.is_export_operator_cancelled = False
dprops.bake.is_export_operator_running = True
dprops.bake.export_progress = 0.0
dprops.bake.export_stage = 'STATIC'
dprops.cache.logfile_name = self._get_logfile_name(context)
def _get_export_filepath(self):
dprops = self._get_domain_properties()
return os.path.join(dprops.cache.get_cache_abspath(),
dprops.bake.export_directory_name,
dprops.bake.export_filename)
def _export_simulation_data_file(self):
dprops = self._get_domain_properties()
dprops.bake.export_filepath = self._get_export_filepath()
dprops.bake.export_success = export.export_simulation_data(
bpy.context,
dprops.bake.export_filepath
)
if dprops.bake.export_success:
dprops.bake.is_cache_directory_set = True
def _export_simulation_data(self, context):
print("Exporting simulation data...")
dprops = self._get_domain_properties()
is_exporting = (not dprops.bake.is_autosave_available or
dprops.simulation.update_settings_on_resume)
if not is_exporting:
return
self._initialize_geometry_exporter(context)
self._initialize_export_operator(context)
while True:
is_finished = self.geometry_exporter.update_export(1.0/15.0)
dprops.bake.export_progress = self.geometry_exporter.get_export_progress()
dprops.bake.export_stage = self.geometry_exporter.get_export_stage()
if is_finished:
if self.geometry_exporter.is_error():
self.report({"ERROR"}, self.geometry_exporter.get_error_message())
dprops.bake.is_bake_cancelled = True
dprops.bake.is_export_operator_running = False
return False
self._export_simulation_data_file()
if not dprops.bake.export_success:
dprops.bake.is_bake_cancelled = True
self.cancel(context)
return False
dprops.bake.is_export_operator_running = False
return True
def _update_simulation_stats(self, context):
update_stats(context)
def _run_fluid_simulation(self, context):
preferences = vcu.get_addon_preferences()
print("Running fluid simulation...")
dprops = self._get_domain_properties()
savestate_id = dprops.simulation.get_selected_savestate_id()
max_baking_retries = preferences.cmd_bake_max_attempts
cache_directory = dprops.cache.get_cache_abspath()
dprops.bake.export_filepath = self._get_export_filepath()
self.data.progress = 0.0
self.data.is_console_output_enabled = True
self._update_simulation_stats(context)
bake.bake(dprops.bake.export_filepath, cache_directory, self.data, savestate_id, max_baking_retries)
self._update_simulation_stats(context)
def execute(self, context):
if not context.scene.flip_fluid.is_domain_object_set():
self.report({"ERROR_INVALID_INPUT"},
"Fluid simulation requires a domain object")
self.cancel(context)
return {'CANCELLED'}
if context.scene.flip_fluid.get_num_domain_objects() > 1:
self.report({"ERROR_INVALID_INPUT"},
"There must be only one domain object")
self.cancel(context)
return {'CANCELLED'}
if not context.scene.flip_fluid.is_domain_in_active_scene():
self.report({"ERROR"},
"Active scene must contain domain object to begin baking. Select the scene that contains the domain object, save, and try again.")
self.cancel(context)
return {'CANCELLED'}
self._reset_bake(context)
self._initialize_domain(context)
success = self._export_simulation_data(context)
if not success:
self.cancel(context)
return {'FINISHED'}
dprops = self._get_domain_properties()
if dprops.bake.is_bake_cancelled:
self.cancel(context)
return {'FINISHED'}
_notify_bake_operator_running()
_notify_cmd_bake_operator_running()
self._run_fluid_simulation(context)
self.cancel(context)
return {'FINISHED'}
def cancel(self, context):
dprops = self._get_domain_properties()
if dprops is None:
return
dprops.bake.is_simulation_running = False
dprops.bake.is_bake_cancelled = False
dprops.bake.is_export_operator_running = False
dprops.bake.check_autosave()
_notify_bake_operator_cancelled()
_notify_cmd_bake_operator_cancelled()
class CancelBakeFluidSimulation(bpy.types.Operator):
bl_idname = "flip_fluid_operators.cancel_bake_fluid_simulation"
bl_label = "Cancel Bake Fluid Simulation"
bl_description = "Stop baking fluid simulation"
@classmethod
def poll(cls, context):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return not dprops.bake.is_bake_cancelled
def execute(self, context):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
dprops.bake.is_bake_cancelled = True
dprops.bake.is_export_operator_cancelled = True
return {'FINISHED'}
class FlipFluidResetBake(bpy.types.Operator):
bl_idname = "flip_fluid_operators.reset_bake"
bl_label = "Reset Bake"
bl_description = ("Reset simulation bake to initial state. WARNING: this" +
" operation will delete previously baked simulation data")
def _clear_cache(self, context):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
cache_directory = dprops.cache.get_cache_abspath()
fpl.clear_cache_directory(cache_directory,
clear_export=False,
clear_logs=False,
remove_directory=False
)
def _reset_property_data(self):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
dprops.mesh_cache.reset_cache_objects()
dprops.stats.refresh_stats()
dprops.stats.reset_time_remaining()
dprops.stats.reset_stats_values()
dprops.bake.check_autosave()
dprops.render.reset_bake()
@classmethod
def poll(cls, context):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return not dprops.bake.is_simulation_running
def execute(self, context):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
cache_path = dprops.cache.get_cache_abspath()
if not os.path.isdir(cache_path):
self._reset_property_data()
self.report({"INFO"}, "Current cache directory does not exist - skipping cache reset")
return {'FINISHED'}
dprops.cache.mark_cache_directory_set()
self._clear_cache(context)
self._reset_property_data()
self.report({"INFO"}, "Successfully reset bake")
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
def register():
bpy.utils.register_class(BakeFluidSimulation)
bpy.utils.register_class(BakeFluidSimulationCommandLine)
bpy.utils.register_class(CancelBakeFluidSimulation)
bpy.utils.register_class(FlipFluidResetBake)
def unregister():
bpy.utils.unregister_class(BakeFluidSimulation)
bpy.utils.unregister_class(BakeFluidSimulationCommandLine)
bpy.utils.unregister_class(CancelBakeFluidSimulation)
bpy.utils.unregister_class(FlipFluidResetBake)
@@ -0,0 +1,709 @@
# 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, re, shutil
from ..filesystem import filesystem_protection_layer as fpl
from ..utils import version_compatibility_utils as vcu
from bpy.props import (
StringProperty,
)
class FlipFluidFreeCache(bpy.types.Operator):
bl_idname = "flip_fluid_operators.free_cache"
bl_label = "Free Cache"
bl_description = "Delete Simulation Cache Files"
def clear_cache(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
cache_directory = dprops.cache.get_cache_abspath()
clear_export = dprops.cache.clear_cache_directory_export
clear_logs = dprops.cache.clear_cache_directory_logs
fpl.clear_cache_directory(cache_directory,
clear_export=clear_export,
clear_logs=clear_logs,
remove_directory=True
)
@classmethod
def poll(cls, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return not dprops.bake.is_simulation_running
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
cache_path = dprops.cache.get_cache_abspath()
if not os.path.isdir(cache_path):
self.report({"ERROR"}, "Current cache directory does not exist")
return {'CANCELLED'}
self.clear_cache(context)
self.report({"INFO"}, "Successfully cleared cache directory '" + cache_path + "'.")
dprops.stats.refresh_stats()
dprops.bake.check_autosave()
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class FlipFluidFreeUnheldCacheFiles(bpy.types.Operator):
bl_idname = "flip_fluid_operators.free_unheld_cache_files"
bl_label = "Free Unheld Cache Files"
bl_description = "Delete Unheld Simulation Cache Files"
def delete_unheld_cache_directory(self, directory, extension):
if not os.path.isdir(directory):
return
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
rprops = dprops.render
hold_frame = rprops.hold_frame_number
for f in os.listdir(directory):
if f.endswith(extension):
startidx = -(len(extension) + 6)
endidx = startidx + 6
numstr = f[startidx:endidx]
if not numstr.isdigit():
continue
frameno = int(numstr)
if frameno == hold_frame:
continue
filepath = os.path.join(directory, f)
fpl.delete_file(filepath)
def clear_unheld_cache_files(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
cache_dir = dprops.cache.get_cache_abspath()
bakefiles_dir = os.path.join(cache_dir, "bakefiles")
self.delete_unheld_cache_directory(bakefiles_dir, ".bbox")
self.delete_unheld_cache_directory(bakefiles_dir, ".bobj")
self.delete_unheld_cache_directory(bakefiles_dir, ".data")
self.delete_unheld_cache_directory(bakefiles_dir, ".wwp")
self.delete_unheld_cache_directory(bakefiles_dir, ".wwf")
self.delete_unheld_cache_directory(bakefiles_dir, ".wwi")
self.delete_unheld_cache_directory(bakefiles_dir, ".fpd")
self.delete_unheld_cache_directory(bakefiles_dir, ".ffd")
self.delete_unheld_cache_directory(bakefiles_dir, ".ffp3")
def count_directory_bytes(self, dirpath):
byte_count = 0
for dirpath, dirnames, filenames in os.walk(dirpath):
for f in filenames:
fp = os.path.join(dirpath, f)
byte_count += os.path.getsize(fp)
return byte_count
def update_cache_bytes(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
cache_directory = dprops.cache.get_cache_abspath()
bakefiles_directory = os.path.join(cache_directory, "bakefiles")
dprops.stats.cache_bytes.set(self.count_directory_bytes(bakefiles_directory))
@classmethod
def poll(cls, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return not dprops.bake.is_simulation_running
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
cache_path = dprops.cache.get_cache_abspath()
if not os.path.isdir(cache_path):
self.report({"ERROR"}, "Current cache directory does not exist")
return {'CANCELLED'}
self.clear_unheld_cache_files(context)
self.report({"INFO"}, "Successfully cleared unheld cache files")
self.update_cache_bytes(context)
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class FlipFluidMoveCache(bpy.types.Operator):
bl_idname = "flip_fluid_operators.move_cache"
bl_label = "Move Cache"
bl_description = "Move Simulation Cache Files"
@classmethod
def poll(cls, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return not dprops.bake.is_simulation_running
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
src_dir = dprops.cache.get_cache_abspath()
dst_dir = dprops.cache.get_abspath(dprops.cache.move_cache_directory)
try:
if not os.path.exists(src_dir):
os.makedirs(src_dir)
except Exception as e:
self.report({"ERROR"}, "Error creating cache directory: " + str(e))
return {'CANCELLED'}
try:
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
except Exception as e:
self.report({"ERROR"}, "Error creating destination directory: " + str(e))
return {'CANCELLED'}
if not os.access(dst_dir, os.W_OK):
self.report({"ERROR"}, "Unable to write to destination directory")
return {'CANCELLED'}
try:
shutil.move(src_dir, dst_dir)
except Exception as e:
self.report({"ERROR"}, "Error moving cache directory: " + str(e))
return {'CANCELLED'}
base_dir = os.path.basename(src_dir)
new_cache_path = os.path.join(dst_dir, base_dir)
self.report({"INFO"}, "Successfully moved '" + src_dir + "' to '" + new_cache_path + "'.")
dprops.cache.cache_directory = new_cache_path
dprops.stats.refresh_stats()
return {'FINISHED'}
class FlipFluidRenameCache(bpy.types.Operator):
bl_idname = "flip_fluid_operators.rename_cache"
bl_label = "Rename Cache"
bl_description = "Rename Simulation Cache Directory"
@classmethod
def poll(cls, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return not dprops.bake.is_simulation_running
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
src_dir = dprops.cache.get_cache_abspath()
parent_dir = os.path.dirname(src_dir)
rename_dir = dprops.cache.rename_cache_directory
new_cache_path = os.path.join(parent_dir, rename_dir)
try:
if not os.path.exists(src_dir):
os.makedirs(src_dir)
except Exception as e:
self.report({"ERROR"}, "Error creating cache directory: " + str(e))
return {'CANCELLED'}
if os.path.exists(new_cache_path):
self.report({"ERROR"}, "Renamed cache directory already exists")
return {'CANCELLED'}
if not os.access(os.path.dirname(new_cache_path), os.W_OK):
dst_dir = os.path.dirname(new_cache_path)
self.report({"ERROR"}, "Unable to write to destination directory: " + dst_dir)
return {'CANCELLED'}
try:
os.rename(src_dir, new_cache_path)
except Exception as e:
self.report({"ERROR"}, "Error renaming cache directory: " + str(e))
return {'CANCELLED'}
self.report({"INFO"}, "Successfully renamed '" + src_dir + "' to '" + new_cache_path + "'.")
is_relative = dprops.cache.cache_directory.startswith("//")
dprops.cache.cache_directory = new_cache_path
if is_relative:
bpy.ops.flip_fluid_operators.relative_cache_directory("INVOKE_DEFAULT")
dprops.stats.refresh_stats()
return {'FINISHED'}
class FlipFluidCopyCache(bpy.types.Operator):
bl_idname = "flip_fluid_operators.copy_cache"
bl_label = "Copy Cache"
bl_description = "Copy Simulation Cache Directory"
@classmethod
def poll(cls, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return not dprops.bake.is_simulation_running
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
src_dir = dprops.cache.get_cache_abspath()
dst_dir = dprops.cache.get_abspath(dprops.cache.copy_cache_directory)
try:
if not os.path.exists(src_dir):
os.makedirs(src_dir)
except Exception as e:
self.report({"ERROR"}, "Error creating cache directory: " + str(e))
return {'CANCELLED'}
try:
shutil.copytree(src_dir, dst_dir)
except Exception as e:
self.report({"ERROR"}, "Error copying cache directory: " + str(e))
return {'CANCELLED'}
self.report({"INFO"}, "Successfully copied '" + src_dir + "' to '" + dst_dir + "'.")
return {'FINISHED'}
class FlipFluidRelativeCacheDirectory(bpy.types.Operator):
bl_idname = "flip_fluid_operators.relative_cache_directory"
bl_label = "Make Relative"
bl_description = "Convert to a filepath relative to the Blend file"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
blend_filepath = bpy.path.abspath("//")
if not blend_filepath:
self.report({"ERROR"}, "Cannot make path relative to unsaved Blend file")
return {'CANCELLED'}
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
cache_directory = dprops.cache.get_cache_abspath()
try:
relpath = os.path.relpath(cache_directory, blend_filepath)
except ValueError:
base = os.path.basename(bpy.data.filepath)
save_file = os.path.splitext(base)[0]
cache_folder_parent = os.path.dirname(bpy.data.filepath)
cache_folder = save_file + "_flip_fluid_cache"
cache_path = os.path.join(cache_folder_parent, cache_folder)
relpath = os.path.relpath(cache_path, cache_folder_parent)
info_msg = "Relative path requires Blend file and cache directory to be on the same drive."
info_msg += " Resetting cache to default relative path."
self.report({"INFO"}, info_msg)
relprefix = "//"
dprops.cache.cache_directory = relprefix + relpath
return {'FINISHED'}
class FlipFluidAbsoluteCacheDirectory(bpy.types.Operator):
bl_idname = "flip_fluid_operators.absolute_cache_directory"
bl_label = "Make Absolute"
bl_description = "Convert to an absolute filepath location"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
dprops.cache.cache_directory = dprops.cache.get_cache_abspath()
return {'FINISHED'}
class FlipFluidMatchFilenameCacheDirectory(bpy.types.Operator):
bl_idname = "flip_fluid_operators.match_filename_cache_directory"
bl_label = "Match Filename"
bl_description = ("Set the cache directory name to correspond to the .blend filename." +
" Note: this will not rename an existing cache directory")
@classmethod
def poll(cls, context):
return True
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
blend_filepath = bpy.path.abspath("//")
if not blend_filepath:
self.report({"ERROR"}, "The Blend file must be saved to use this operator")
return {'CANCELLED'}
cache_directory = dprops.cache.cache_directory
relprefix = "//"
is_relative = cache_directory.startswith(relprefix)
abspath = dprops.cache.get_cache_abspath()
parent_path = os.path.dirname(abspath)
new_directory_name = os.path.basename(bpy.data.filepath)
new_directory_name = os.path.splitext(new_directory_name)[0]
new_directory_name += "_flip_fluid_cache"
dprops.cache.cache_directory = os.path.join(parent_path, new_directory_name)
if is_relative:
bpy.ops.flip_fluid_operators.relative_cache_directory()
return {'FINISHED'}
class FlipFluidIncreaseDecreaseCacheDirectory(bpy.types.Operator):
bl_idname = "flip_fluid_operators.increase_decrease_cache_directory"
bl_label = "Increase/Decrease Cache Directory"
bl_description = ("Increase or decrease a numbered suffix on the cache directory." +
" Note: this will not rename an existing cache directory")
increment_mode = StringProperty(default="INCREASE")
exec(vcu.convert_attribute_to_28("increment_mode"))
@classmethod
def poll(cls, context):
return True
def get_trailing_number(self, s):
m = re.search(r'\d+$', s)
return int(m.group()) if m else None
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
cache_directory = dprops.cache.cache_directory
relprefix = "//"
is_relative = cache_directory.startswith(relprefix)
abspath = dprops.cache.get_cache_abspath()
parent_path = os.path.dirname(abspath)
basename = os.path.basename(abspath)
suffix_number = self.get_trailing_number(basename)
if suffix_number:
basename = basename[:-len(str(suffix_number))]
if self.increment_mode == 'INCREASE':
if not suffix_number:
suffix_number = 0
suffix_number += 1
new_basename = basename + str(suffix_number)
else:
if not suffix_number:
return {'FINISHED'}
if suffix_number <= 1:
suffix_string = ""
else:
suffix_string = str(suffix_number - 1)
new_basename = basename + suffix_string
dprops.cache.cache_directory = os.path.join(parent_path, new_basename)
if is_relative:
bpy.ops.flip_fluid_operators.relative_cache_directory()
return {'FINISHED'}
class FlipFluidIncreaseDecreaseRenderDirectory(bpy.types.Operator):
bl_idname = "flip_fluid_operators.increase_decrease_render_directory"
bl_label = "Increase/Decrease Render Directory"
bl_description = ("Increase or decrease a numbered suffix on the render output directory." +
" Note: this will not rename an existing render output directory")
increment_mode = StringProperty(default="INCREASE")
exec(vcu.convert_attribute_to_28("increment_mode"))
@classmethod
def poll(cls, context):
return True
def get_trailing_number(self, s):
m = re.search(r'\d+$', s)
return int(m.group()) if m else None
def ends_with_slash(self, s):
return s.endswith("/") or s.endswith("\\")
def ends_with_underscore(self, s):
return s.endswith("_")
def execute(self, context):
render_directory = context.scene.render.filepath
basename = render_directory
endswith_slash = self.ends_with_slash(basename)
endswith_underscore = self.ends_with_underscore(basename)
slash_character = ""
if endswith_slash:
slash_character = basename[-1]
basename = basename[:-1]
elif endswith_underscore:
basename = basename[:-1]
suffix_number = self.get_trailing_number(basename)
if suffix_number:
basename = basename[:-len(str(suffix_number))]
if endswith_underscore:
if self.ends_with_underscore(basename):
basename = basename[:-1]
if self.increment_mode == 'INCREASE':
if not suffix_number:
suffix_number = 0
suffix_number += 1
suffix_string = str(suffix_number)
else:
if not suffix_number:
return {'FINISHED'}
if suffix_number <= 1:
suffix_string = ""
else:
suffix_string = str(suffix_number - 1)
if endswith_slash:
new_basename = basename + suffix_string + slash_character
elif endswith_underscore:
if suffix_string:
new_basename = basename + "_" + suffix_string + "_"
else:
new_basename = basename + "_"
else:
new_basename = basename + suffix_string
context.scene.render.filepath = new_basename
return {'FINISHED'}
class FlipFluidIncreaseDecreaseCacheRenderVersion(bpy.types.Operator):
bl_idname = "flip_fluid_operators.increase_decrease_cache_render_version"
bl_label = "Increase/Decrease Cache and Render Version"
bl_description = ("Increase or decrease a numbered suffix on both the cache" +
" directory and render output directory. Note: this will not rename an" +
" existing cache our render output directory")
increment_mode = StringProperty(default="INCREASE")
exec(vcu.convert_attribute_to_28("increment_mode"))
@classmethod
def poll(cls, context):
return True
def execute(self, context):
bpy.ops.flip_fluid_operators.increase_decrease_cache_directory(increment_mode=self.increment_mode)
bpy.ops.flip_fluid_operators.increase_decrease_render_directory(increment_mode=self.increment_mode)
return {'FINISHED'}
class FlipFluidRelativeLinkedGeometryDirectory(bpy.types.Operator):
bl_idname = "flip_fluid_operators.relative_linked_geometry_directory"
bl_label = "Make Relative"
bl_description = "Convert to a filepath relative to the Blend file"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
blend_filepath = bpy.path.abspath("//")
if not blend_filepath:
self.report({"ERROR"}, "Cannot make path relative to unsaved Blend file")
return {'CANCELLED'}
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
linked_directory = dprops.cache.get_linked_geometry_abspath()
if not linked_directory:
self.report({"ERROR"}, "Linked geometry directory is not set. Set to an existing cache directory and try again.")
return {'CANCELLED'}
try:
relpath = os.path.relpath(linked_directory, blend_filepath)
except ValueError:
self.report({"ERROR"}, "Relative path requires Blend file and cache directory to be on the same drive")
return {'CANCELLED'}
relprefix = "//"
dprops.cache.linked_geometry_directory = relprefix + relpath
return {'FINISHED'}
class FlipFluidAbsoluteLinkedGeometryDirectory(bpy.types.Operator):
bl_idname = "flip_fluid_operators.absolute_linked_geometry_directory"
bl_label = "Make Absolute"
bl_description = "Convert to an absolute filepath location"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
linked_directory = dprops.cache.get_linked_geometry_abspath()
if not linked_directory:
self.report({"ERROR"}, "Linked geometry directory is not set. Set to an existing cache directory and try again.")
return {'CANCELLED'}
dprops.cache.linked_geometry_directory = linked_directory
return {'FINISHED'}
class FlipFluidClearLinkedGeometryDirectory(bpy.types.Operator):
bl_idname = "flip_fluid_operators.clear_linked_geometry_directory"
bl_label = "Clear"
bl_description = "Clear the linked geometry directory field. No files will be deleted"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
dprops.cache.linked_geometry_directory = ""
return {'FINISHED'}
def register():
bpy.utils.register_class(FlipFluidFreeCache)
bpy.utils.register_class(FlipFluidFreeUnheldCacheFiles)
# The move, rename, and copy cache operations should not be performed
# in Blender and are removed from the UI. There is a potential for Blender
# to crash, which could lead to loss of data. It is best to perform these
# operations through the OS filesystem which is cabable of handling failures.
"""
bpy.utils.register_class(FlipFluidMoveCache)
bpy.utils.register_class(FlipFluidRenameCache)
bpy.utils.register_class(FlipFluidCopyCache)
"""
bpy.utils.register_class(FlipFluidRelativeCacheDirectory)
bpy.utils.register_class(FlipFluidAbsoluteCacheDirectory)
bpy.utils.register_class(FlipFluidMatchFilenameCacheDirectory)
bpy.utils.register_class(FlipFluidIncreaseDecreaseCacheDirectory)
bpy.utils.register_class(FlipFluidIncreaseDecreaseRenderDirectory)
bpy.utils.register_class(FlipFluidIncreaseDecreaseCacheRenderVersion)
bpy.utils.register_class(FlipFluidRelativeLinkedGeometryDirectory)
bpy.utils.register_class(FlipFluidAbsoluteLinkedGeometryDirectory)
bpy.utils.register_class(FlipFluidClearLinkedGeometryDirectory)
def unregister():
bpy.utils.unregister_class(FlipFluidFreeCache)
bpy.utils.unregister_class(FlipFluidFreeUnheldCacheFiles)
"""
bpy.utils.unregister_class(FlipFluidMoveCache)
bpy.utils.unregister_class(FlipFluidRenameCache)
bpy.utils.unregister_class(FlipFluidCopyCache)
"""
bpy.utils.unregister_class(FlipFluidRelativeCacheDirectory)
bpy.utils.unregister_class(FlipFluidAbsoluteCacheDirectory)
bpy.utils.unregister_class(FlipFluidMatchFilenameCacheDirectory)
bpy.utils.unregister_class(FlipFluidIncreaseDecreaseCacheDirectory)
bpy.utils.unregister_class(FlipFluidIncreaseDecreaseRenderDirectory)
bpy.utils.unregister_class(FlipFluidIncreaseDecreaseCacheRenderVersion)
bpy.utils.unregister_class(FlipFluidRelativeLinkedGeometryDirectory)
bpy.utils.unregister_class(FlipFluidAbsoluteLinkedGeometryDirectory)
bpy.utils.unregister_class(FlipFluidClearLinkedGeometryDirectory)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,271 @@
# 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, blf, math, colorsys
from bpy.props import (
IntProperty
)
from ..objects.flip_fluid_aabb import AABB
from ..utils import ui_utils
from ..utils import version_compatibility_utils as vcu
from .. import render
if vcu.is_blender_28():
import gpu
from gpu_extras.batch import batch_for_shader
particle_vertices = []
particle_vertex_colors = []
particle_shader = None
particle_batch_draw = None
def update_debug_force_field_geometry(context):
if render.is_rendering():
# This method does not need to be run while rendering. Can cause
# crashes on certain systems.
return
global particle_vertices
global particle_vertex_colors
particle_vertices = []
particle_vertex_colors = []
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None or not dprops.debug.export_force_field:
return
if not dprops.debug.force_field_visibility:
return
ffdata = dprops.mesh_cache.gl_force_field.get_force_field_data()
if ffdata is None or ffdata['vertices'] is None:
return
min_strength = dprops.debug.min_gradient_force
max_strength = dprops.debug.max_gradient_force
if dprops.debug.force_field_gradient_mode == 'GRADIENT_NONE':
min_strength = max_strength = dprops.debug.max_gradient_force
min_color = dprops.debug.low_force_field_color
max_color = dprops.debug.high_force_field_color
gradient_mode = 'HSV' if dprops.debug.force_field_gradient_mode == 'GRADIENT_HSV' else 'RGB'
display_pct = dprops.debug.force_field_display_amount / 100
num_points = int(display_pct * len(ffdata['vertices']))
for i in range(num_points):
v = ffdata['vertices'][i]
particle_vertices.append((v[0], v[1], v[2]))
strength = v[3]
if dprops.debug.force_field_gradient_mode == 'GRADIENT_NONE':
color_factor = 0.0
if strength > max_strength:
color_factor = 1.0
else:
diff = (max_strength - min_strength)
if diff < 1e-6:
diff = 1e-6
color_factor = (strength - min_strength) / diff
color_factor = max(0, min(color_factor, 1.0))
color = _lerp_rgb(min_color, max_color, color_factor, mode=gradient_mode)
color_tuple = (color[0], color[1], color[2], 1.0)
particle_vertex_colors.append(color_tuple)
if vcu.is_blender_28():
global particle_shader
global particle_batch_draw
vertex_shader = """
uniform mat4 ModelViewProjectionMatrix;
in vec3 pos;
in vec4 color;
out vec4 finalColor;
void main()
{
gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0);
finalColor = color;
}
"""
fragment_shader = """
in vec4 finalColor;
out vec4 fragColor;
void main()
{
fragColor = finalColor;
}
"""
if vcu.is_blender_35():
# Needed for support on MacOS Apple Silicon systems in Blender 3.5 or later
# Could possibly be a Blender regression bug why the below method no longer
# works in Blender 3.5 or later for MacOS. Should file a report.
shader_name = '3D_SMOOTH_COLOR'
if vcu.is_blender_40():
shader_name = 'SMOOTH_COLOR'
particle_shader = gpu.shader.from_builtin(shader_name)
else:
particle_shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
particle_batch_draw = batch_for_shader(
particle_shader, 'POINTS',
{"pos": particle_vertices, "color": particle_vertex_colors},
)
def _lerp_rgb(minc, maxc, factor, mode='RGB'):
if mode == 'RGB':
r = minc[0] + factor * (maxc[0] - minc[0])
g = minc[1] + factor * (maxc[1] - minc[1])
b = minc[2] + factor * (maxc[2] - minc[2])
return (r, g, b)
elif mode == 'HSV':
minhsv = colorsys.rgb_to_hsv(*minc)
maxhsv = colorsys.rgb_to_hsv(*maxc)
h = minhsv[0] + factor * (maxhsv[0] - minhsv[0])
s = minhsv[1] + factor * (maxhsv[1] - minhsv[1])
v = minhsv[2] + factor * (maxhsv[2] - minhsv[2])
return colorsys.hsv_to_rgb(h, s, v)
class FlipFluidDrawForceField(bpy.types.Operator):
bl_idname = "flip_fluid_operators.draw_force_field"
bl_label = "Draw Force Field"
bl_description = "Draw Force Field Lines"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return True
def draw_callback_3d(self, context):
if render.is_rendering():
# This method does not need to be run while rendering. Can cause
# crashes on certain systems.
return
global particle_vertices
global particle_vertex_colors
domain = context.scene.flip_fluid.get_domain_object()
dprops = context.scene.flip_fluid.get_domain_properties()
if domain is None or len(particle_vertices) == 0:
return
if not dprops.debug.force_field_visibility:
return
if vcu.get_object_hide_viewport(domain):
return
if not vcu.is_blender_28():
dlayers = [i for i,v in enumerate(domain.layers) if v]
slayers = [i for i,v in enumerate(context.scene.layers) if v]
if not (set(dlayers) & set(slayers)):
return
if vcu.is_blender_28():
global particle_shader
global particle_batch_draw
if vcu.is_blender_35():
# Warnings in Blender 3.5 when using bgl module, which is to
# be deprecated in Blender 3.7. Use gpu module instead.
gpu.state.point_size_set(dprops.debug.force_field_line_size)
else:
# only attempt to import bgl when necessary (older versions of Blender). In Blender >= 3.5,
# importing bgl generates a warning, and possibly an error in Blender >= 4.0.
import bgl
bgl.glPointSize(dprops.debug.force_field_line_size)
if vcu.is_blender_35():
# Can be drawn with depth in Blender 3.5 or later
gpu.state.depth_test_set('LESS_EQUAL')
gpu.state.depth_mask_set(True)
particle_batch_draw.draw(particle_shader)
if vcu.is_blender_35():
gpu.state.depth_mask_set(False)
else:
# only attempt to import bgl when necessary (older versions of Blender). In Blender >= 3.5,
# importing bgl generates a warning, and possibly an error in Blender >= 4.0.
import bgl
bgl.glPointSize(dprops.debug.force_field_line_size)
bgl.glBegin(bgl.GL_POINTS)
current_color = None
for i in range(len(particle_vertices)):
if current_color != particle_vertex_colors[i]:
current_color = particle_vertex_colors[i]
bgl.glColor4f(current_color[0], current_color[1], current_color[2], 1.0)
bgl.glVertex3f(*(particle_vertices[i]))
bgl.glEnd()
bgl.glPointSize(1)
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
def modal(self, context, event):
if not event.type == 'TIMER':
return {'PASS_THROUGH'}
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None or not dprops.debug.export_force_field:
self.cancel(context)
return {'CANCELLED'}
return {'PASS_THROUGH'}
def invoke(self, context, event):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return
args = (context,)
self._handle_3d = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
context.window_manager.modal_handler_add(self)
self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
dprops.debug.is_draw_force_field_operator_running = True
return {'RUNNING_MODAL'}
def cancel(self, context):
bpy.types.SpaceView3D.draw_handler_remove(self._handle_3d, 'WINDOW')
context.window_manager.event_timer_remove(self._timer)
ui_utils.force_ui_redraw()
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is not None:
dprops.debug.is_draw_force_field_operator_running = False
def register():
bpy.utils.register_class(FlipFluidDrawForceField)
def unregister():
bpy.utils.unregister_class(FlipFluidDrawForceField)
@@ -0,0 +1,497 @@
# 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, blf, math, colorsys
from bpy.props import (
IntProperty
)
from ..objects.flip_fluid_aabb import AABB
from ..utils import ui_utils
from ..utils import version_compatibility_utils as vcu
from .. import render
if vcu.is_blender_28():
import gpu
from gpu_extras.batch import batch_for_shader
x_coords = []
y_coords = []
z_coords = []
bounds_coords = []
def update_debug_grid_geometry(context):
if render.is_rendering():
# This method does not need to be run while rendering. Can cause
# crashes on certain systems.
return
global x_coords
global y_coords
global z_coords
global bounds_coords
x_coords = []
y_coords = []
z_coords = []
bounds_coords = []
domain = context.scene.flip_fluid.get_domain_object()
if domain is None:
return
dprops = context.scene.flip_fluid.get_domain_properties()
if not dprops.debug.is_simulation_grid_debugging_enabled():
return
bbox = AABB.from_blender_object(domain)
max_dim = max(bbox.xdim, bbox.ydim, bbox.zdim)
if dprops.debug.grid_display_mode == 'GRID_DISPLAY_SIMULATION':
isize, jsize, ksize, dx = dprops.simulation.get_viewport_grid_dimensions()
elif dprops.debug.grid_display_mode == 'GRID_DISPLAY_PREVIEW':
presolution = dprops.simulation.preview_resolution
isize, jsize, ksize, dx = dprops.simulation.get_viewport_grid_dimensions(resolution=presolution)
else:
isize, jsize, ksize, dx = dprops.simulation.get_viewport_grid_dimensions()
if dprops.debug.grid_display_mode == 'GRID_DISPLAY_MESH':
isize *= (dprops.surface.subdivisions + 1)
jsize *= (dprops.surface.subdivisions + 1)
ksize *= (dprops.surface.subdivisions + 1)
dx /= (dprops.surface.subdivisions + 1)
elif dprops.debug.grid_display_mode == 'GRID_DISPLAY_FORCE_FIELD':
isize, jsize, ksize, dx = dprops.simulation.get_viewport_grid_dimensions()
reduction = 1
if dprops.world.force_field_resolution == 'FORCE_FIELD_RESOLUTION_LOW':
reduction = 4
elif dprops.world.force_field_resolution == 'FORCE_FIELD_RESOLUTION_NORMAL':
reduction = 3
elif dprops.world.force_field_resolution == 'FORCE_FIELD_RESOLUTION_HIGH':
reduction = 2
isize = int(math.ceil(isize / reduction))
jsize = int(math.ceil(jsize / reduction))
ksize = int(math.ceil(ksize / reduction))
dx *= reduction
disp_scale = dprops.debug.grid_display_scale
igrid = math.ceil(isize / disp_scale)
jgrid = math.ceil(jsize / disp_scale)
kgrid = math.ceil(ksize / disp_scale)
dxgrid = dx * disp_scale
if dprops.debug.snap_offsets_to_grid:
xoffset = math.ceil(dprops.debug.debug_grid_offsets[0] * igrid) * dxgrid
yoffset = math.ceil(dprops.debug.debug_grid_offsets[1] * jgrid) * dxgrid
zoffset = math.ceil(dprops.debug.debug_grid_offsets[2] * kgrid) * dxgrid
else:
xoffset = dprops.debug.debug_grid_offsets[0] * igrid * dxgrid
yoffset = dprops.debug.debug_grid_offsets[1] * jgrid * dxgrid
zoffset = dprops.debug.debug_grid_offsets[2] * kgrid * dxgrid
# Geometry Data
z_coords = []
for i in range(igrid + 1):
z_coords.append((bbox.x + i * dxgrid, bbox.y, bbox.z + zoffset))
z_coords.append((bbox.x + i * dxgrid, bbox.y + jgrid * dxgrid, bbox.z + zoffset))
for j in range(jgrid + 1):
z_coords.append((bbox.x, bbox.y + j * dxgrid, bbox.z + zoffset))
z_coords.append((bbox.x + igrid * dxgrid, bbox.y + j * dxgrid, bbox.z + zoffset))
y_coords = []
for i in range(igrid + 1):
y_coords.append((bbox.x + i * dxgrid, bbox.y + yoffset, bbox.z))
y_coords.append((bbox.x + i * dxgrid, bbox.y + yoffset, bbox.z + kgrid * dxgrid))
for k in range(kgrid + 1):
y_coords.append((bbox.x, bbox.y + yoffset, bbox.z + k * dxgrid))
y_coords.append((bbox.x + igrid * dxgrid, bbox.y + yoffset, bbox.z + k * dxgrid))
x_coords = []
for j in range(jgrid + 1):
x_coords.append((bbox.x + xoffset, bbox.y + j * dxgrid, bbox.z))
x_coords.append((bbox.x + xoffset, bbox.y + j * dxgrid, bbox.z + kgrid * dxgrid))
for k in range(kgrid + 1):
x_coords.append((bbox.x + xoffset, bbox.y, bbox.z + k * dxgrid))
x_coords.append((bbox.x + xoffset, bbox.y + jgrid * dxgrid, bbox.z + k * dxgrid))
native_dx = max_dim / dprops.simulation.resolution
solid_width = 1.5 * native_dx
width = math.ceil(bbox.xdim / native_dx) * native_dx
height = math.ceil(bbox.ydim / native_dx) * native_dx
depth = math.ceil(bbox.zdim / native_dx) * native_dx
minx = bbox.x + solid_width
miny = bbox.y + solid_width
minz = bbox.z + solid_width
maxx = bbox.x + isize * dx - solid_width
maxy = bbox.y + jsize * dx - solid_width
maxz = bbox.z + ksize * dx - solid_width
if minx > maxx:
minx = bbox.x + 0.5 * isize * dx
maxx = minx
if miny > maxy:
miny = bbox.y + 0.5 * jsize * dx
maxy = miny
if minz > maxz:
minz = bbox.z + 0.5 * ksize * dx
maxz = minz
bounds_coords = [
(minx, miny, minz), (maxx, miny, minz), (minx, maxy, minz), (maxx, maxy, minz),
(minx, miny, maxz), (maxx, miny, maxz), (minx, maxy, maxz), (maxx, maxy, maxz),
(minx, miny, minz), (minx, maxy, minz), (maxx, miny, minz), (maxx, maxy, minz),
(minx, miny, maxz), (minx, maxy, maxz), (maxx, miny, maxz), (maxx, maxy, maxz),
(minx, miny, minz), (minx, miny, maxz), (maxx, miny, minz), (maxx, miny, maxz),
(minx, maxy, minz), (minx, maxy, maxz), (maxx, maxy, minz), (maxx, maxy, maxz)
]
class FlipFluidDrawDebugGrid(bpy.types.Operator):
bl_idname = "flip_fluid_operators.draw_debug_grid"
bl_label = "Draw Debug Grid"
bl_description = "Draw debug view of the domain simulation grid"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return True
def draw_callback_2d(self, context):
draw_text = False
if not draw_text:
return
if render.is_rendering():
# This method does not need to be run while rendering. Can cause
# crashes on certain systems.
return
domain = context.scene.flip_fluid.get_domain_object()
if domain is None:
return
dprops = context.scene.flip_fluid.get_domain_properties()
if vcu.get_object_hide_viewport(domain):
return
if not dprops.debug.display_simulation_grid:
return
if not vcu.is_blender_28():
dlayers = [i for i,v in enumerate(domain.layers) if v]
slayers = [i for i,v in enumerate(context.scene.layers) if v]
if not (set(dlayers) & set(slayers)):
return
if dprops.debug.grid_display_mode == 'GRID_DISPLAY_SIMULATION':
isize, jsize, ksize, viewport_dx = dprops.simulation.get_viewport_grid_dimensions()
_, _, _, simulation_dx = dprops.simulation.get_simulation_grid_dimensions()
elif dprops.debug.grid_display_mode == 'GRID_DISPLAY_PREVIEW':
presolution = dprops.simulation.preview_resolution
"""
isize, jsize, ksize, viewport_dx = dprops.simulation.get_viewport_grid_dimensions(resolution=presolution)
_, _, _, simulation_dx = dprops.simulation.get_simulation_grid_dimensions(resolution=presolution)
else:
isize, jsize, ksize, viewport_dx = dprops.simulation.get_viewport_grid_dimensions()
_, _, _, simulation_dx = dprops.simulation.get_simulation_grid_dimensions()
if dprops.debug.grid_display_mode == 'GRID_DISPLAY_MESH':
isize *= (dprops.surface.subdivisions + 1)
jsize *= (dprops.surface.subdivisions + 1)
ksize *= (dprops.surface.subdivisions + 1)
viewport_dx /= (dprops.surface.subdivisions + 1)
"""
isize, jsize, ksize, viewport_dx = dprops.simulation.get_viewport_grid_dimensions(resolution=presolution)
_, _, _, simulation_dx = dprops.simulation.get_simulation_grid_dimensions(resolution=presolution)
elif dprops.debug.grid_display_mode == 'GRID_DISPLAY_MESH':
isize, jsize, ksize, simulation_dx = dprops.simulation.get_viewport_grid_dimensions()
isize *= (dprops.surface.subdivisions + 1)
jsize *= (dprops.surface.subdivisions + 1)
ksize *= (dprops.surface.subdivisions + 1)
simulation_dx /= (dprops.surface.subdivisions + 1)
elif dprops.debug.grid_display_mode == 'GRID_DISPLAY_FORCE_FIELD':
isize, jsize, ksize, simulation_dx = dprops.simulation.get_viewport_grid_dimensions()
reduction = 1
force_field_quality_str = 'Ultra'
if dprops.world.force_field_resolution == 'FORCE_FIELD_RESOLUTION_LOW':
reduction = 4
force_field_quality_str = 'Low'
elif dprops.world.force_field_resolution == 'FORCE_FIELD_RESOLUTION_NORMAL':
reduction = 3
force_field_quality_str = 'Normal'
elif dprops.world.force_field_resolution == 'FORCE_FIELD_RESOLUTION_HIGH':
reduction = 2
force_field_quality_str = 'High'
isize = int(math.ceil(isize / reduction))
jsize = int(math.ceil(jsize / reduction))
ksize = int(math.ceil(ksize / reduction))
simulation_dx *= reduction
width = context.region.width
if vcu.is_blender_28():
height = 200
xstart = context.region.width - 400
else:
height = context.region.height
xstart = 50
font_id = 0
try:
# Not all Blender versions have functionality to set font color with this method
blf.color(font_id, 1.0, 1.0, 1.0, 1.0)
except:
pass
if dprops.debug.grid_display_mode == 'GRID_DISPLAY_SIMULATION':
blf.size(font_id, 20, 72)
blf.position(font_id, xstart, height - 50, 0)
blf.draw(font_id, "Simulation Grid")
blf.size(font_id, 15, 72)
blf.position(font_id, xstart + 10, height - 80, 0)
blf.draw(font_id, "Grid Resolution: " + str(isize) + " x " + str(jsize) + " x " + str(ksize))
dimx = round(simulation_dx * isize, 4)
dimy = round(simulation_dx * jsize, 4)
dimz = round(simulation_dx * ksize, 4)
blf.position(font_id, xstart + 10, height - 105, 0)
blf.draw(font_id, "Grid Dimensions: " + str(dimx) + "m x " + str(dimy) + "m x " + str(dimz) + "m")
blf.position(font_id, xstart + 10, height - 130, 0)
blf.draw(font_id, "Voxel Count: " + format(isize*jsize*ksize, ",").replace(",", " "))
blf.position(font_id, xstart + 10, height - 155, 0)
blf.draw(font_id, "Voxel Width: " + str(round(simulation_dx, 4)) + "m")
elif dprops.debug.grid_display_mode == 'GRID_DISPLAY_MESH':
if dprops.surface.compute_chunk_mode == 'COMPUTE_CHUNK_MODE_AUTO':
compute_chunks = dprops.surface.compute_chunks_auto
else:
compute_chunks = dprops.surface.compute_chunks_fixed
blf.size(font_id, 20, 72)
blf.position(font_id, xstart, height - 50, 0)
blf.draw(font_id, "Final Surface Mesh Grid")
blf.size(font_id, 15, 72)
blf.position(font_id, xstart + 10, height - 80, 0)
blf.draw(font_id, "Subdivisions: " + str(dprops.surface.subdivisions))
blf.position(font_id, xstart + 10, height - 105, 0)
blf.draw(font_id, "Compute Chunks: " + str(compute_chunks))
blf.position(font_id, xstart + 10, height - 130, 0)
blf.draw(font_id, "Grid Resolution: " + str(isize) + " x " + str(jsize) + " x " + str(ksize))
num_cells = isize*jsize*ksize
num_cells_str = format(num_cells, ",").replace(",", " ")
chunk_cells_str = format(math.ceil(num_cells / compute_chunks), ",").replace(",", " ")
blf.position(font_id, xstart + 10, height - 155, 0)
blf.draw(font_id, "Voxel Count: " + num_cells_str + " (" + chunk_cells_str + " / chunk)")
blf.position(font_id, xstart + 10, height - 180, 0)
blf.draw(font_id, "Voxel Width: " + str(round(simulation_dx, 4)))
elif dprops.debug.grid_display_mode == 'GRID_DISPLAY_PREVIEW':
blf.size(font_id, 20, 72)
blf.position(font_id, xstart, height - 50, 0)
blf.draw(font_id, "Preview Surface Mesh Grid")
blf.size(font_id, 15, 72)
blf.position(font_id, xstart + 10, height - 80, 0)
blf.draw(font_id, "Grid Resolution: " + str(isize) + " x " + str(jsize) + " x " + str(ksize))
num_cells = isize*jsize*ksize
num_cells_str = format(num_cells, ",").replace(",", " ")
blf.position(font_id, xstart + 10, height - 105, 0)
blf.draw(font_id, "Voxel Width: " + str(round(simulation_dx, 4)))
elif dprops.debug.grid_display_mode == 'GRID_DISPLAY_FORCE_FIELD':
blf.size(font_id, 20, 72)
blf.position(font_id, xstart, height - 50, 0)
blf.draw(font_id, "Force Field Grid")
blf.size(font_id, 15, 72)
blf.position(font_id, xstart + 10, height - 80, 0)
blf.draw(font_id, "Force Field Quality: " + force_field_quality_str)
blf.position(font_id, xstart + 10, height - 105, 0)
blf.draw(font_id, "Force Field Resolution: " + str(isize) + " x " + str(jsize) + " x " + str(ksize))
num_cells = isize*jsize*ksize
num_cells_str = format(num_cells, ",").replace(",", " ")
blf.position(font_id, xstart + 10, height - 130, 0)
blf.draw(font_id, "Voxel Width: " + str(round(simulation_dx, 4)))
def draw_callback_3d(self, context):
if render.is_rendering():
# This method does not need to be run while rendering. Can cause
# crashes on certain systems.
return
global x_coords
global y_coords
global z_coords
global bounds_coords
domain = context.scene.flip_fluid.get_domain_object()
if domain is None:
return
dprops = context.scene.flip_fluid.get_domain_properties()
if vcu.get_object_hide_viewport(domain):
return
if not vcu.is_blender_28():
dlayers = [i for i,v in enumerate(domain.layers) if v]
slayers = [i for i,v in enumerate(context.scene.layers) if v]
if not (set(dlayers) & set(slayers)):
return
x_color = dprops.debug.x_grid_color
y_color = dprops.debug.y_grid_color
z_color = dprops.debug.z_grid_color
bounds_color = dprops.debug.domain_bounds_color
# Draw
display_grid = dprops.debug.display_simulation_grid
if vcu.is_blender_28():
line_draw_mode = '3D_UNIFORM_COLOR'
if vcu.is_blender_36():
# 3D/2D prefix deprecated in recent versions of Blender
line_draw_mode = 'UNIFORM_COLOR'
if display_grid and dprops.debug.enabled_debug_grids[2]:
shader = gpu.shader.from_builtin(line_draw_mode)
batch = batch_for_shader(shader, 'LINES', {"pos": z_coords})
shader.bind()
shader.uniform_float("color", (z_color[0], z_color[1], z_color[2], 1.0))
if vcu.is_blender_35():
# Can be drawn with depth in Blender 3.5 or later
gpu.state.depth_test_set('LESS_EQUAL')
gpu.state.depth_mask_set(True)
batch.draw(shader)
if vcu.is_blender_35():
gpu.state.depth_mask_set(False)
if display_grid and dprops.debug.enabled_debug_grids[1]:
shader = gpu.shader.from_builtin(line_draw_mode)
batch = batch_for_shader(shader, 'LINES', {"pos": y_coords})
shader.bind()
shader.uniform_float("color", (y_color[0], y_color[1], y_color[2], 1.0))
if vcu.is_blender_35():
# Can be drawn with depth in Blender 3.5 or later
gpu.state.depth_test_set('LESS_EQUAL')
gpu.state.depth_mask_set(True)
batch.draw(shader)
if vcu.is_blender_35():
gpu.state.depth_mask_set(False)
if display_grid and dprops.debug.enabled_debug_grids[0]:
shader = gpu.shader.from_builtin(line_draw_mode)
batch = batch_for_shader(shader, 'LINES', {"pos": x_coords})
shader.bind()
shader.uniform_float("color", (x_color[0], x_color[1], x_color[2], 1.0))
if vcu.is_blender_35():
# Can be drawn with depth in Blender 3.5 or later
gpu.state.depth_test_set('LESS_EQUAL')
gpu.state.depth_mask_set(True)
batch.draw(shader)
if vcu.is_blender_35():
gpu.state.depth_mask_set(False)
if dprops.debug.display_domain_bounds:
shader = gpu.shader.from_builtin(line_draw_mode)
batch = batch_for_shader(shader, 'LINES', {"pos": bounds_coords})
shader.bind()
shader.uniform_float("color", (bounds_color[0], bounds_color[1], bounds_color[2], 1.0))
if vcu.is_blender_35():
# Can be drawn with depth in Blender 3.5 or later
gpu.state.depth_test_set('LESS_EQUAL')
gpu.state.depth_mask_set(True)
batch.draw(shader)
if vcu.is_blender_35():
gpu.state.depth_mask_set(False)
else:
import bgl
bgl.glLineWidth(1)
bgl.glBegin(bgl.GL_LINES)
if display_grid and dprops.debug.enabled_debug_grids[2]:
bgl.glColor4f(*z_color, 1.0)
for c in z_coords:
bgl.glVertex3f(*c)
if display_grid and dprops.debug.enabled_debug_grids[1]:
bgl.glColor4f(*y_color, 1.0)
for c in y_coords:
bgl.glVertex3f(*c)
if display_grid and dprops.debug.enabled_debug_grids[0]:
bgl.glColor4f(*x_color, 1.0)
for c in x_coords:
bgl.glVertex3f(*c)
if dprops.debug.display_domain_bounds:
bgl.glColor4f(*(bounds_color), 1.0)
for c in bounds_coords:
bgl.glVertex3f(*c)
bgl.glEnd()
bgl.glLineWidth(1)
bgl.glEnable(bgl.GL_DEPTH_TEST)
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
def modal(self, context, event):
if not event.type == 'TIMER':
return {'PASS_THROUGH'}
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None or not dprops.debug.is_simulation_grid_debugging_enabled():
self.cancel(context)
return {'CANCELLED'}
return {'PASS_THROUGH'}
def invoke(self, context, event):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return
args = (context,)
self._handle_2d = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_2d, args, 'WINDOW', 'POST_PIXEL')
self._handle_3d = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
context.window_manager.modal_handler_add(self)
self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
dprops.debug.is_draw_debug_grid_operator_running = True
return {'RUNNING_MODAL'}
def cancel(self, context):
bpy.types.SpaceView3D.draw_handler_remove(self._handle_3d, 'WINDOW')
bpy.types.SpaceView3D.draw_handler_remove(self._handle_2d, 'WINDOW')
context.window_manager.event_timer_remove(self._timer)
ui_utils.force_ui_redraw()
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is not None:
dprops.debug.is_draw_debug_grid_operator_running = False
def register():
bpy.utils.register_class(FlipFluidDrawDebugGrid)
def unregister():
bpy.utils.unregister_class(FlipFluidDrawDebugGrid)
@@ -0,0 +1,310 @@
# 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, blf, math, colorsys
from bpy.props import (
IntProperty
)
from ..objects.flip_fluid_aabb import AABB
from ..utils import ui_utils
from ..utils import version_compatibility_utils as vcu
from .. import render
if vcu.is_blender_28():
import gpu
from gpu_extras.batch import batch_for_shader
particle_vertices = []
particle_vertex_colors = []
particle_shader = None
particle_batch_draw = None
def update_debug_particle_geometry(context):
if render.is_rendering():
# This method does not need to be run while rendering. Can cause
# crashes on certain systems.
return
global particle_vertices
global particle_vertex_colors
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None or not dprops.debug.enable_fluid_particle_debug_output:
return
if not dprops.debug.fluid_particles_visibility:
return
particle_vertices = []
particle_vertex_colors = []
pdata = dprops.mesh_cache.gl_particles.get_point_cache_data()
if pdata is None or len(pdata['particles']) == 0:
return
bbox_obj = dprops.debug.get_particle_draw_aabb_object()
bbox = None if bbox_obj is None else AABB.from_blender_object(bbox_obj)
min_color = dprops.debug.low_speed_particle_color
max_color = dprops.debug.high_speed_particle_color
num_gradient_colors = 128
color_ranges = _get_color_ranges(pdata, num_gradient_colors)
particles = pdata['particles']
if bbox is None:
particle_vertices = pdata['particles']
for cidx in range(len(color_ranges)):
start_idx = 0 if cidx == 0 else color_ranges[cidx - 1]
end_idx = color_ranges[cidx]
if end_idx - start_idx == 0:
continue
color_factor = cidx / (len(color_ranges) - 1)
gmode = 'HSV' if dprops.debug.fluid_particle_gradient_mode == 'GRADIENT_HSV' else 'RGB'
color = _lerp_rgb(min_color, max_color, color_factor, mode=gmode)
color_tuple = (color[0], color[1], color[2], 1.0)
if bbox is None:
particle_vertex_colors += (end_idx - start_idx) * [color_tuple]
else:
for pidx in range(start_idx, end_idx):
if bbox.contains_point(particles[pidx]):
particle_vertices.append(particles[pidx])
particle_vertex_colors.append(color_tuple)
if vcu.is_blender_28():
global particle_shader
global particle_batch_draw
vertex_shader = """
uniform mat4 ModelViewProjectionMatrix;
in vec3 pos;
in vec4 color;
out vec4 finalColor;
void main()
{
gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0);
finalColor = color;
}
"""
fragment_shader = """
in vec4 finalColor;
out vec4 fragColor;
void main()
{
fragColor = finalColor;
}
"""
if vcu.is_blender_35():
# Needed for support on MacOS Apple Silicon systems in Blender 3.5 or later
# Could possibly be a Blender regression bug why the below method no longer
# works in Blender 3.5 or later for MacOS. Should file a report.
shader_name = '3D_SMOOTH_COLOR'
if vcu.is_blender_40():
shader_name = 'SMOOTH_COLOR'
particle_shader = gpu.shader.from_builtin(shader_name)
else:
particle_shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
particle_batch_draw = batch_for_shader(
particle_shader, 'POINTS',
{"pos": particle_vertices, "color": particle_vertex_colors},
)
def _get_color_ranges(pdata, num_colors):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return
particles = pdata['particles']
binspeeds = pdata['binspeeds']
binstarts = pdata['binstarts']
if dprops.debug.fluid_particle_gradient_mode == 'GRADIENT_NONE':
min_speed = max_speed = dprops.debug.max_gradient_speed
else:
min_speed = dprops.debug.min_gradient_speed
max_speed = dprops.debug.max_gradient_speed
binidx = 0
for i in range(len(binspeeds)):
if binspeeds[i] > min_speed:
binidx = max(i - 1, 0)
break
color_ranges = []
for cidx in range(num_colors):
if cidx == num_colors - 1:
color_speed_limit = float('inf')
else:
speed_factor = (cidx + 1) / (num_colors - 1)
color_speed_limit = min_speed + speed_factor * (max_speed - min_speed)
for i in range(binidx, len(binspeeds)):
if binspeeds[i] > color_speed_limit:
color_ranges.append(binstarts[i])
binidx = i
break
if i == len(binspeeds) - 1:
color_ranges.append(len(particles))
return color_ranges
def _lerp_rgb(minc, maxc, factor, mode='RGB'):
if mode == 'RGB':
r = minc[0] + factor * (maxc[0] - minc[0])
g = minc[1] + factor * (maxc[1] - minc[1])
b = minc[2] + factor * (maxc[2] - minc[2])
return (r, g, b)
elif mode == 'HSV':
minhsv = colorsys.rgb_to_hsv(*minc)
maxhsv = colorsys.rgb_to_hsv(*maxc)
h = minhsv[0] + factor * (maxhsv[0] - minhsv[0])
s = minhsv[1] + factor * (maxhsv[1] - minhsv[1])
v = minhsv[2] + factor * (maxhsv[2] - minhsv[2])
return colorsys.hsv_to_rgb(h, s, v)
class FlipFluidDrawGLParticles(bpy.types.Operator):
bl_idname = "flip_fluid_operators.draw_gl_particles"
bl_label = "Draw GL Particles"
bl_description = "Draw mesh cache particles"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return False
return True
def draw_callback_3d(self, context):
if render.is_rendering():
# This method does not need to be run while rendering. Can cause
# crashes on certain systems.
return
global particle_vertices
global particle_vertex_colors
domain = context.scene.flip_fluid.get_domain_object()
dprops = context.scene.flip_fluid.get_domain_properties()
if domain is None or len(particle_vertices) == 0:
return
if not dprops.debug.fluid_particles_visibility:
return
if vcu.get_object_hide_viewport(domain):
return
if not vcu.is_blender_28():
dlayers = [i for i,v in enumerate(domain.layers) if v]
slayers = [i for i,v in enumerate(context.scene.layers) if v]
if not (set(dlayers) & set(slayers)):
return
if vcu.is_blender_28():
global particle_shader
global particle_batch_draw
if vcu.is_blender_35():
# Warnings in Blender 3.5 when using bgl module, which is to
# be deprecated in Blender 3.7. Use gpu module instead.
gpu.state.point_size_set(dprops.debug.particle_size)
else:
# only attempt to import bgl when necessary (older versions of Blender). In Blender >= 3.5,
# importing bgl generates a warning, and possibly an error in Blender >= 4.0.
import bgl
bgl.glPointSize(dprops.debug.particle_size)
if vcu.is_blender_35():
# Can be drawn with depth in Blender 3.5 or later
gpu.state.depth_test_set('LESS_EQUAL')
gpu.state.depth_mask_set(True)
particle_batch_draw.draw(particle_shader)
if vcu.is_blender_35():
gpu.state.depth_mask_set(False)
else:
# only attempt to import bgl when necessary (older versions of Blender). In Blender >= 3.5,
# importing bgl generates a warning, and possibly an error in Blender >= 4.0.
import bgl
bgl.glPointSize(dprops.debug.particle_size)
bgl.glBegin(bgl.GL_POINTS)
current_color = None
for i in range(len(particle_vertices)):
if current_color != particle_vertex_colors[i]:
current_color = particle_vertex_colors[i]
bgl.glColor4f(current_color[0], current_color[1], current_color[2], 1.0)
bgl.glVertex3f(*(particle_vertices[i]))
bgl.glEnd()
bgl.glPointSize(1)
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
def modal(self, context, event):
if not event.type == 'TIMER':
return {'PASS_THROUGH'}
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None or not dprops.debug.enable_fluid_particle_debug_output:
self.cancel(context)
return {'CANCELLED'}
return {'PASS_THROUGH'}
def invoke(self, context, event):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return
args = (context,)
self._handle_3d = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
context.window_manager.modal_handler_add(self)
self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
dprops.debug.is_draw_gl_particles_operator_running = True
return {'RUNNING_MODAL'}
def cancel(self, context):
bpy.types.SpaceView3D.draw_handler_remove(self._handle_3d, 'WINDOW')
context.window_manager.event_timer_remove(self._timer)
ui_utils.force_ui_redraw()
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops is not None:
dprops.debug.is_draw_gl_particles_operator_running = False
def register():
bpy.utils.register_class(FlipFluidDrawGLParticles)
def unregister():
bpy.utils.unregister_class(FlipFluidDrawGLParticles)
@@ -0,0 +1,77 @@
# 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, textwrap
from bpy.props import (
StringProperty,
IntProperty
)
from ..utils import version_compatibility_utils as vcu
class FlipFluidDisplayError(bpy.types.Operator):
bl_idname = "flip_fluid_operators.display_error"
bl_label = ""
bl_description = ""
error_message = StringProperty()
exec(vcu.convert_attribute_to_28("error_message"))
error_description = StringProperty()
exec(vcu.convert_attribute_to_28("error_description"))
popup_width = IntProperty(default=400)
exec(vcu.convert_attribute_to_28("popup_width"))
def draw(self, context):
row = self.layout.row()
row.alignment = 'CENTER'
row.label(text=self.error_message, icon='ERROR')
if self.error_description:
text_list = textwrap.wrap(self.error_description, width=self.popup_width//6)
column = self.layout.column(align=True)
column.separator()
column.separator()
for idx,line in enumerate(text_list):
column.label(text=line)
self.layout.separator()
self.layout.separator()
def execute(self, context):
self.report({'INFO'}, self.error_message)
if self.error_description:
self.report({'INFO'}, self.error_description)
self.error_message = ""
self.error_description = ""
self.popup_width = 400
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=self.popup_width)
def register():
bpy.utils.register_class(FlipFluidDisplayError)
def unregister():
bpy.utils.unregister_class(FlipFluidDisplayError)
@@ -0,0 +1,400 @@
# 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, csv, math
from ..objects import flip_fluid_geometry_exporter as geometry_exporter
from .. import export
class ExportFluidSimulation(bpy.types.Operator):
bl_idname = "flip_fluid_operators.export_fluid_simulation"
bl_label = "Export Fluid Simulation"
bl_description = "Export fluid simulation data"
bl_options = {'REGISTER'}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.timer = None
self.is_executing_timer_event = False
self.geometry_exporter = None
self.export_step_time = 1.0 / 15.0
self.mesh_data = None
def _get_domain_properties(self):
return bpy.context.scene.flip_fluid.get_domain_properties()
def _initialize_geometry_exporter(self, context):
print("Exporting Simulation Meshes:")
print("------------------------------------------------------------")
export_dir = self._get_export_directory()
self.geometry_exporter = geometry_exporter.GeometryExportManager(export_dir)
export.add_objects_to_geometry_exporter(self.geometry_exporter)
def _get_logfile_name(self, context):
dprops = self._get_domain_properties()
cache_directory = dprops.cache.get_cache_abspath()
logs_directory = os.path.join(cache_directory, "logs")
basename = os.path.basename(bpy.data.filepath)
basename = os.path.splitext(basename)[0]
if not basename:
basename = "untitled"
# Filesystem may have a limit on path length. Truncate name to some max length
# to avoid errors in cases where the path could become long
max_length = 16
if len(basename) > max_length:
basename = basename[:max_length]
filename = basename
filepath = os.path.join(logs_directory, filename + ".txt")
if os.path.isfile(filepath):
for i in range(1, 1000):
filename = basename + "." + str(i).zfill(3)
filepath = os.path.join(logs_directory, filename + ".txt")
if not os.path.isfile(filepath):
break;
return filename + ".txt"
def _initialize_operator(self, context):
context.window_manager.modal_handler_add(self)
self.timer = context.window_manager.event_timer_add(0.01, window=context.window)
dprops = self._get_domain_properties()
dprops.bake.is_export_operator_cancelled = False
dprops.bake.is_export_operator_running = True
dprops.bake.export_progress = 0.0
dprops.bake.export_stage = 'STATIC'
dprops.cache.logfile_name = self._get_logfile_name(context)
def _get_export_filepath(self):
dprops = self._get_domain_properties()
return os.path.join(dprops.cache.get_cache_abspath(),
dprops.bake.export_directory_name,
dprops.bake.export_filename)
def _get_export_directory(self):
dprops = self._get_domain_properties()
return os.path.join(dprops.cache.get_cache_abspath(),
dprops.bake.export_directory_name)
def _export_simulation_data_file(self):
dprops = self._get_domain_properties()
dprops.bake.export_filepath = self._get_export_filepath()
dprops.bake.export_success = export.export_simulation_data(
bpy.context,
dprops.bake.export_filepath
)
if dprops.bake.export_success:
dprops.bake.is_cache_directory_set = True
def _update_flip_object_force_reexport_on_bake(self, context):
sim_objects = context.scene.flip_fluid.get_simulation_objects()
for obj in sim_objects:
props = obj.flip_fluid.get_property_group()
if hasattr(props, "force_reexport_on_next_bake"):
props.force_reexport_on_next_bake = False
@classmethod
def poll(cls, context):
return bpy.context.scene.flip_fluid.is_domain_object_set()
def modal(self, context, event):
dprops = self._get_domain_properties()
if dprops is None:
self.report({"ERROR_INVALID_INPUT"}, "Export operator requires a domain object")
self.cancel(context)
return {'CANCELLED'}
if not context.scene.flip_fluid.is_domain_in_active_scene():
self.report({"ERROR"},
"Active scene must contain domain object during export - halting export and baking process. Please do not switch active scenes during export.")
self.cancel(context)
return {'CANCELLED'}
if dprops.bake.is_export_operator_cancelled:
self.cancel(context)
return {'FINISHED'}
if event.type == 'TIMER' and not self.is_executing_timer_event:
self.is_executing_timer_event = True
is_finished = self.geometry_exporter.update_export(self.export_step_time)
dprops.bake.export_progress = self.geometry_exporter.get_export_progress()
dprops.bake.export_stage = self.geometry_exporter.get_export_stage()
if is_finished:
if self.geometry_exporter.is_error():
self.report({"ERROR"}, self.geometry_exporter.get_error_message())
dprops.bake.is_bake_cancelled = True
self.cancel(context)
return {'FINISHED'}
self._export_simulation_data_file()
if not dprops.bake.export_success:
dprops.bake.is_bake_cancelled = True
self.cancel(context)
return {'FINISHED'}
self._update_flip_object_force_reexport_on_bake(context)
self.cancel(context)
return {'FINISHED'}
try:
# Depending on window, area may be None
context.area.tag_redraw()
except:
pass
self.is_executing_timer_event = False
return {'PASS_THROUGH'}
def execute(self, context):
self._initialize_geometry_exporter(context)
self._initialize_operator(context)
return {'RUNNING_MODAL'}
def cancel(self, context):
if self.timer:
context.window_manager.event_timer_remove(self.timer)
self.timer = None
dprops = self._get_domain_properties()
if dprops is None:
return
dprops.bake.is_export_operator_running = False
class FlipFluidExportStatsCSV(bpy.types.Operator):
bl_idname = "flip_fluid_operators.export_stats_csv"
bl_label = "Export CSV"
bl_description = "Export simulation stats to CSV file format"
@classmethod
def poll(cls, context):
return True
def format_float(self, n):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops.stats.csv_region_format == 'CSV_REGION_US':
s = "{:.3f}".format(n)
else:
s = "{:.3f}".format(n)
s = s.replace('.', ',')
return s
def export_simulation_stats_to_csv(self, stats_data, filepath):
max_frame_id = 0
frame_count = False
for key in stats_data.keys():
if key.isdigit():
max_frame_id = max(stats_data[key]['frame'], max_frame_id)
frame_count += 1
if frame_count == 0:
self.report({"ERROR"}, str(export_csv.error_message))
return False
frame_list = [None] * (max_frame_id + 1)
field_names = [
'frame_id',
'frame_timeline',
'timestep',
'substeps',
'particles_fluid',
'particles_whitewater',
'time_mesh_generation',
'time_velocity_advection',
'time_fluid_particles',
'time_pressure_solver',
'time_whitewater_solver',
'time_viscosity_solver',
'time_simulation_objects',
'time_other',
'time_total',
'mesh_surface_enabled',
'mesh_surface_vertices',
'mesh_surface_triangles',
'mesh_surface_bytes',
'mesh_preview_enabled',
'mesh_preview_vertices',
'mesh_preview_triangles',
'mesh_preview_bytes',
'mesh_foam_enabled',
'mesh_foam_vertices',
'mesh_foam_triangles',
'mesh_foam_bytes',
'mesh_bubble_enabled',
'mesh_bubble_vertices',
'mesh_bubble_triangles',
'mesh_bubble_bytes',
'mesh_spray_enabled',
'mesh_spray_vertices',
'mesh_spray_triangles',
'mesh_spray_bytes',
'mesh_obstacle_enabled',
'mesh_obstacle_vertices',
'mesh_obstacle_triangles',
'mesh_obstacle_bytes',
'debug_particles_enabled',
'debug_particles_vertices',
'debug_particles_triangles',
'debug_particles_bytes'
]
for key in stats_data.keys():
if not key.isdigit():
continue
frame_data = stats_data[key]
frame_id = frame_data['frame']
time_other = max(frame_data['timing']['total'] -
frame_data['timing']['mesh'] -
frame_data['timing']['advection'] -
frame_data['timing']['particles'] -
frame_data['timing']['pressure'] -
frame_data['timing']['diffuse'] -
frame_data['timing']['viscosity'] -
frame_data['timing']['objects'], 0)
trueval = 'TRUE'
falseval = 'FALSE'
frame_list[frame_id] = {
'frame_id': frame_id,
'frame_timeline': int(key),
'timestep': self.format_float(frame_data['delta_time']),
'substeps': frame_data['substeps'],
'particles_fluid': frame_data['fluid_particles'],
'particles_whitewater': frame_data['diffuse_particles'],
'time_mesh_generation': self.format_float(frame_data['timing']['mesh']),
'time_velocity_advection': self.format_float(frame_data['timing']['advection']),
'time_fluid_particles': self.format_float(frame_data['timing']['particles']),
'time_pressure_solver': self.format_float(frame_data['timing']['pressure']),
'time_whitewater_solver': self.format_float(frame_data['timing']['diffuse']),
'time_viscosity_solver': self.format_float(frame_data['timing']['viscosity']),
'time_simulation_objects': self.format_float(frame_data['timing']['objects']),
'time_other': self.format_float(time_other),
'time_total': self.format_float(frame_data['timing']['total']),
'mesh_surface_enabled': trueval if frame_data['surface']['enabled'] else falseval,
'mesh_surface_vertices': max(frame_data['surface']['vertices'], 0),
'mesh_surface_triangles': max(frame_data['surface']['triangles'], 0),
'mesh_surface_bytes': frame_data['surface']['bytes'],
'mesh_preview_enabled': trueval if frame_data['preview']['enabled'] else falseval,
'mesh_preview_vertices': max(frame_data['preview']['vertices'], 0),
'mesh_preview_triangles': max(frame_data['preview']['triangles'], 0),
'mesh_preview_bytes': frame_data['preview']['bytes'],
'mesh_foam_enabled': trueval if frame_data['foam']['enabled'] else falseval,
'mesh_foam_vertices': max(frame_data['foam']['vertices'], 0),
'mesh_foam_triangles': max(frame_data['foam']['triangles'], 0),
'mesh_foam_bytes': frame_data['foam']['bytes'],
'mesh_bubble_enabled': trueval if frame_data['bubble']['enabled'] else falseval,
'mesh_bubble_vertices': max(frame_data['bubble']['vertices'], 0),
'mesh_bubble_triangles': max(frame_data['bubble']['triangles'], 0),
'mesh_bubble_bytes': frame_data['bubble']['bytes'],
'mesh_spray_enabled': trueval if frame_data['spray']['enabled'] else falseval,
'mesh_spray_vertices': max(frame_data['spray']['vertices'], 0),
'mesh_spray_triangles': max(frame_data['spray']['triangles'], 0),
'mesh_spray_bytes': frame_data['spray']['bytes'],
'mesh_obstacle_enabled': trueval if frame_data['obstacle']['enabled'] else falseval,
'mesh_obstacle_vertices': max(frame_data['obstacle']['vertices'], 0),
'mesh_obstacle_triangles': max(frame_data['obstacle']['triangles'], 0),
'mesh_obstacle_bytes': frame_data['obstacle']['bytes'],
'debug_particles_enabled': trueval if frame_data['particles']['enabled'] else falseval,
'debug_particles_vertices': max(frame_data['particles']['vertices'], 0),
'debug_particles_triangles': max(frame_data['particles']['triangles'], 0),
'debug_particles_bytes': frame_data['particles']['bytes']
}
csv_rows = []
for d in frame_list:
if d is not None:
csv_rows.append(d)
with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
if dprops.stats.csv_region_format == 'CSV_REGION_US':
delimiter = ','
else:
delimiter = ';'
writer = csv.DictWriter(csvfile, fieldnames=field_names, delimiter=delimiter)
writer.writeheader()
writer.writerows(csv_rows)
return True
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
cache_directory = dprops.cache.get_cache_abspath()
statsfile = os.path.join(cache_directory, dprops.stats.stats_filename)
if not os.path.isfile(statsfile):
self.report({"ERROR"}, "Missing simulation stats data file: " + statsfile)
return {'CANCELLED'}
with open(statsfile, 'r', encoding='utf-8') as f:
statsdata = json.loads(f.read())
csv_filepath = dprops.stats.csv_save_filepath
csv_directory = os.path.dirname(csv_filepath)
try:
if not os.path.exists(csv_directory):
os.makedirs(csv_directory)
except Exception as e:
self.report({"ERROR"}, "Error creating csv file directory: " + str(e))
return {'CANCELLED'}
success = self.export_simulation_stats_to_csv(statsdata, csv_filepath)
if not success:
return {'CANCELLED'}
self.report({"INFO"}, "Successfully exported to CSV: " + csv_filepath)
return {'FINISHED'}
def register():
bpy.utils.register_class(ExportFluidSimulation)
bpy.utils.register_class(FlipFluidExportStatsCSV)
def unregister():
bpy.utils.unregister_class(ExportFluidSimulation)
bpy.utils.unregister_class(FlipFluidExportStatsCSV)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,128 @@
# 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 ..materials import material_library
class FlipFluidImportMaterial(bpy.types.Operator):
bl_idname = "flip_fluid_operators.import_material"
bl_label = "Import"
bl_description = "Import the selected material and link to material library"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return True
def import_single_material(self, library_name):
is_imported, imported_material_name = material_library.is_material_imported(library_name)
if is_imported:
msg = "Library material already imported: <" + library_name + ">"
if library_name != imported_material_name:
msg += " as <" + imported_material_name + ">"
self.report({'INFO'}, msg)
return {'FINISHED'}
imported_material_name = material_library.import_material(library_name)
msg = "Successfully imported library material: <" + library_name + ">"
if library_name != imported_material_name:
msg += " as <" + imported_material_name + ">"
self.report({'INFO'}, msg)
return {'FINISHED'}
def import_all_materials(self):
material_list = bpy.context.scene.flip_fluid_material_library.material_list
for mdata in material_list:
self.import_single_material(mdata.name)
return {'FINISHED'}
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
material_library_name = dprops.materials.material_import
if material_library_name == 'ALL_MATERIALS':
return_enum = self.import_all_materials()
else:
return_enum = self.import_single_material(material_library_name)
return return_enum
class FlipFluidImportMaterialCopy(bpy.types.Operator):
bl_idname = "flip_fluid_operators.import_material_copy"
bl_label = "Import Copy"
bl_description = "Import a copy of the selected material and do not link to material library"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
return True
def import_single_material(self, library_name):
imported_name = material_library.import_material_copy(library_name)
msg = "Successfully imported copy of library material: <" + library_name + ">"
if library_name != imported_name:
msg += " as <" + imported_name + ">"
self.report({'INFO'}, msg)
return {'FINISHED'}
def import_all_materials(self):
material_list = bpy.context.scene.flip_fluid_material_library.material_list
for mdata in material_list:
self.import_single_material(mdata.name)
return {'FINISHED'}
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
material_library_name = dprops.materials.material_import
if material_library_name == 'ALL_MATERIALS':
return_enum = self.import_all_materials()
else:
return_enum = self.import_single_material(material_library_name)
return return_enum
def register():
bpy.utils.register_class(FlipFluidImportMaterial)
bpy.utils.register_class(FlipFluidImportMaterialCopy)
def unregister():
bpy.utils.unregister_class(FlipFluidImportMaterial)
bpy.utils.unregister_class(FlipFluidImportMaterialCopy)
@@ -0,0 +1,72 @@
# 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 ..utils import version_compatibility_utils as vcu
from ..utils import installation_utils
class FlipFluidAdd(bpy.types.Operator):
bl_idname = "flip_fluid_operators.flip_fluid_add"
bl_label = "Add FLIP fluid object"
bl_description = "Add active object as FLIP Fluid"
bl_options = {'REGISTER'}
@classmethod
def poll(csl, context):
is_addon_disabled = context.scene.flip_fluid.is_addon_disabled_in_blend_file()
return installation_utils.is_installation_complete() and not is_addon_disabled
def execute(self, context):
installation_utils.tag_addon_active()
obj = vcu.get_active_object(context)
obj.flip_fluid.is_active = True
vcu.add_to_flip_fluids_collection(obj, context)
return {'FINISHED'}
class FlipFluidRemove(bpy.types.Operator):
bl_idname = "flip_fluid_operators.flip_fluid_remove"
bl_label = "Remove FLIP fluid object"
bl_description = "Remove FLIP Fluid settings from Object"
bl_options = {'REGISTER'}
@classmethod
def poll(csl, context):
is_addon_disabled = context.scene.flip_fluid.is_addon_disabled_in_blend_file()
return installation_utils.is_installation_complete() and not is_addon_disabled
def execute(self, context):
obj = vcu.get_active_object(context)
obj.flip_fluid.object_type = 'TYPE_NONE'
obj.flip_fluid.is_active = False
vcu.remove_from_flip_fluids_collection(obj, context)
return {'FINISHED'}
def register():
bpy.utils.register_class(FlipFluidAdd)
bpy.utils.register_class(FlipFluidRemove)
def unregister():
bpy.utils.unregister_class(FlipFluidAdd)
bpy.utils.unregister_class(FlipFluidRemove)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,123 @@
# 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 render
from ..utils import ui_utils
class FlipFluidQuickViewportDisplayFinal(bpy.types.Operator):
bl_idname = "flip_fluid_operators.quick_viewport_display_final"
bl_label = "Final"
bl_description = "Display final mesh quality in the viewport"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
domain_object = context.scene.flip_fluid.get_domain_object()
if domain_object is None:
return {'CANCELLED'}
rprops = domain_object.flip_fluid.domain.render
rprops.viewport_display = 'DISPLAY_FINAL'
rprops.whitewater_viewport_display = 'DISPLAY_FINAL'
ui_utils.force_ui_redraw()
return {'FINISHED'}
class FlipFluidQuickViewportDisplayPreview(bpy.types.Operator):
bl_idname = "flip_fluid_operators.quick_viewport_display_preview"
bl_label = "Preview"
bl_description = "Display preview mesh quality in the viewport"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
domain_object = context.scene.flip_fluid.get_domain_object()
if domain_object is None:
return {'CANCELLED'}
rprops = domain_object.flip_fluid.domain.render
rprops.viewport_display = 'DISPLAY_PREVIEW'
rprops.whitewater_viewport_display = 'DISPLAY_PREVIEW'
ui_utils.force_ui_redraw()
return {'FINISHED'}
class FlipFluidQuickViewportDisplayNone(bpy.types.Operator):
bl_idname = "flip_fluid_operators.quick_viewport_display_none"
bl_label = "None"
bl_description = "Do not display meshes in the viewport"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
domain_object = context.scene.flip_fluid.get_domain_object()
if domain_object is None:
return {'CANCELLED'}
rprops = domain_object.flip_fluid.domain.render
rprops.viewport_display = 'DISPLAY_NONE'
rprops.whitewater_viewport_display = 'DISPLAY_NONE'
ui_utils.force_ui_redraw()
return {'FINISHED'}
class FlipFluidReloadFrame(bpy.types.Operator):
bl_idname = "flip_fluid_operators.reload_frame"
bl_label = "Reload Frame"
bl_description = "Reload the current frame"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
domain_object = context.scene.flip_fluid.get_domain_object()
if domain_object is None:
return {'CANCELLED'}
frameno = render.get_current_simulation_frame()
render.reload_frame(frameno)
return {'FINISHED'}
def register():
bpy.utils.register_class(FlipFluidQuickViewportDisplayFinal)
bpy.utils.register_class(FlipFluidQuickViewportDisplayPreview)
bpy.utils.register_class(FlipFluidQuickViewportDisplayNone)
bpy.utils.register_class(FlipFluidReloadFrame)
def unregister():
bpy.utils.unregister_class(FlipFluidQuickViewportDisplayFinal)
bpy.utils.unregister_class(FlipFluidQuickViewportDisplayPreview)
bpy.utils.unregister_class(FlipFluidQuickViewportDisplayNone)
bpy.utils.unregister_class(FlipFluidReloadFrame)
@@ -0,0 +1,45 @@
# 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 bake_operators
class FlipFluidRefreshStats(bpy.types.Operator):
bl_idname = "flip_fluid_operators.refresh_stats"
bl_label = "Refresh Stats"
bl_description = "Refresh and update the cache and frame stats"
bl_options = {'REGISTER'}
@classmethod
def poll(csl, context):
return True
def execute(self, context):
num_updated_frames = bake_operators.update_stats()
self.report({'INFO'}, "Cache and frame stats have been refreshed. Found (" + str(num_updated_frames) + ") new frames.")
return {'FINISHED'}
def register():
bpy.utils.register_class(FlipFluidRefreshStats)
def unregister():
bpy.utils.unregister_class(FlipFluidRefreshStats)
@@ -0,0 +1,440 @@
# 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, string
from ..utils import version_compatibility_utils as vcu
class FlipFluidSupportPrintSystemInfo(bpy.types.Operator):
bl_idname = "flip_fluid_operators.print_system_info"
bl_label = "Print System & Blend Info"
bl_description = "Print system and Blend file info if saved into the file (requires domain)"
@classmethod
def poll(cls, context):
return context.scene.flip_fluid.get_domain_object() is not None
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
dprops.debug.print_system_and_blend_info()
return {'FINISHED'}
class FlipFluidSupportStandardizeBlendFile(bpy.types.Operator):
bl_idname = "flip_fluid_operators.standardize_blend_file"
bl_label = "Standardize Blend File"
bl_description = "Set cache/render location relative, set simulation viewport to visible, set simulation viewport mesh display to final (requires domain)"
@classmethod
def poll(cls, context):
return context.scene.flip_fluid.get_domain_object() is not None
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
print()
original_cache_directory = dprops.cache.get_cache_abspath()
bpy.ops.flip_fluid_operators.relative_cache_directory()
bpy.ops.flip_fluid_operators.match_filename_cache_directory()
new_cache_directory = dprops.cache.get_cache_abspath()
if new_cache_directory != original_cache_directory:
info_msg = "Changed cache directory <" + original_cache_directory + "> -> <" + new_cache_directory + ">"
self.report({'INFO'}, info_msg)
original_render_output = context.scene.render.filepath
bpy.ops.flip_fluid_operators.relative_to_blend_render_output()
new_render_output = context.scene.render.filepath
if new_render_output != original_render_output:
info_msg = "Changed render output <" + original_render_output + "> -> <" + new_render_output + ">"
self.report({'INFO'}, info_msg)
if not context.scene.flip_fluid.show_viewport:
context.scene.flip_fluid.show_viewport = True
self.report({'INFO'}, "Enabled simulation display in viewport")
if not dprops.render.viewport_display == 'DISPLAY_FINAL':
dprops.render.viewport_display = 'DISPLAY_FINAL'
self.report({'INFO'}, "Set surface viewport display to Final")
if not dprops.render.whitewater_viewport_display == 'DISPLAY_FINAL':
dprops.render.whitewater_viewport_display = 'DISPLAY_FINAL'
self.report({'INFO'}, "Set whitewater viewport display to Final")
bpy.ops.wm.save_as_mainfile()
return {'FINISHED'}
class FlipFluidSupportDisplayOverlayStats(bpy.types.Operator):
bl_idname = "flip_fluid_operators.display_overlay_stats"
bl_label = "Display Overlay Stats"
bl_description = "Enable overlays and show geometry overlay stats in the 3D viewports"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.overlay.show_overlays = True
space.overlay.show_stats = True
return {'FINISHED'}
class FlipFluidSupportSelectSimulationObjects(bpy.types.Operator):
bl_idname = "flip_fluid_operators.select_simulation_objects"
bl_label = "Select All Simulation Objects"
bl_description = "Select all objects related to the simulation. Notes: may not select all dependencies of a simulation object. Will not select hidden objects"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
for obj in bpy.data.objects:
obj.select_set(False)
for obj in bpy.data.objects:
if not obj.flip_fluid.is_active:
continue
obj.select_set(True)
if obj.flip_fluid.is_fluid():
target = obj.flip_fluid.fluid.get_target_object()
if target is not None:
target.select_set(True)
if obj.flip_fluid.is_inflow():
target = obj.flip_fluid.inflow.get_target_object()
if target is not None:
target.select_set(True)
if obj.flip_fluid.is_domain():
meshing_volume = obj.flip_fluid.domain.surface.get_meshing_volume_object()
if meshing_volume is not None:
meshing_volume.select_set(True)
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is not None:
mesh_cache_props = [
dprops.mesh_cache.surface,
dprops.mesh_cache.particles,
dprops.mesh_cache.foam,
dprops.mesh_cache.bubble,
dprops.mesh_cache.spray,
dprops.mesh_cache.dust,
dprops.mesh_cache.obstacle,
]
whitewater_cache_props = [
dprops.mesh_cache.foam,
dprops.mesh_cache.bubble,
dprops.mesh_cache.spray,
dprops.mesh_cache.dust,
]
for obj_props in mesh_cache_props:
obj = obj_props.get_cache_object()
if obj is not None:
obj.select_set(True)
domain_obj = context.scene.flip_fluid.get_domain_object()
if domain_obj is not None:
vcu.set_active_object(domain_obj)
elif len(bpy.context.selected_objects) > 0:
vcu.set_active_object(bpy.context.selected_objects[0])
return {'FINISHED'}
class FlipFluidSupportSelectHiddenSimulationObjects(bpy.types.Operator):
bl_idname = "flip_fluid_operators.select_hidden_simulation_objects"
bl_label = "Show and Select Hidden Simulation Objects"
bl_description = "Show and select all objects related to the simulation that are currently hidden. Notes: may not select all dependencies of a simulation object"
@classmethod
def poll(cls, context):
return True
def update_selection(self, obj):
if obj.hide_viewport or obj.hide_get():
obj.hide_viewport = False
obj.hide_set(False)
obj.select_set(True)
def execute(self, context):
for obj in bpy.data.objects:
obj.select_set(False)
for obj in bpy.data.objects:
if not obj.flip_fluid.is_active:
continue
self.update_selection(obj)
if obj.flip_fluid.is_fluid():
target = obj.flip_fluid.fluid.get_target_object()
if target is not None:
self.update_selection(target)
if obj.flip_fluid.is_inflow():
target = obj.flip_fluid.inflow.get_target_object()
if target is not None:
self.update_selection(target)
if obj.flip_fluid.is_domain():
meshing_volume = obj.flip_fluid.domain.surface.get_meshing_volume_object()
if meshing_volume is not None:
self.update_selection(meshing_volume)
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is not None:
mesh_cache_props = [
dprops.mesh_cache.surface,
dprops.mesh_cache.foam,
dprops.mesh_cache.bubble,
dprops.mesh_cache.spray,
dprops.mesh_cache.dust,
dprops.mesh_cache.obstacle,
]
for obj_props in mesh_cache_props:
obj = obj_props.get_cache_object()
if obj is not None:
self.update_selection(obj)
domain_obj = context.scene.flip_fluid.get_domain_object()
if domain_obj is not None:
vcu.set_active_object(domain_obj)
elif len(bpy.context.selected_objects) > 0:
vcu.set_active_object(bpy.context.selected_objects[0])
return {'FINISHED'}
class FlipFluidSupportPrintHiddenSimulationObjects(bpy.types.Operator):
bl_idname = "flip_fluid_operators.print_hidden_simulation_objects"
bl_label = "Print Hidden Simulation Objects"
bl_description = "Print objects related to the simulation that are currently hidden to the system console. Notes: may not select all dependencies of a simulation object"
@classmethod
def poll(cls, context):
return True
def update_print(self, obj):
if obj.hide_viewport or obj.hide_get():
print(obj)
def execute(self, context):
print()
print("*** Hidden Simulation Objects ***")
for obj in bpy.data.objects:
if not obj.flip_fluid.is_active:
continue
self.update_print(obj)
if obj.flip_fluid.is_fluid():
target = obj.flip_fluid.fluid.get_target_object()
if target is not None:
self.update_print(target)
if obj.flip_fluid.is_inflow():
target = obj.flip_fluid.inflow.get_target_object()
if target is not None:
self.update_print(target)
if obj.flip_fluid.is_domain():
meshing_volume = obj.flip_fluid.domain.surface.get_meshing_volume_object()
if meshing_volume is not None:
self.update_print(meshing_volume)
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is not None:
mesh_cache_props = [
dprops.mesh_cache.surface,
dprops.mesh_cache.foam,
dprops.mesh_cache.bubble,
dprops.mesh_cache.spray,
dprops.mesh_cache.dust,
dprops.mesh_cache.obstacle,
]
for obj_props in mesh_cache_props:
obj = obj_props.get_cache_object()
if obj is not None:
self.update_print(obj)
domain_obj = context.scene.flip_fluid.get_domain_object()
if domain_obj is not None:
vcu.set_active_object(domain_obj)
elif len(bpy.context.selected_objects) > 0:
vcu.set_active_object(bpy.context.selected_objects[0])
return {'FINISHED'}
class FlipFluidSupportPrintInverseObstacles(bpy.types.Operator):
bl_idname = "flip_fluid_operators.print_inverse_obstacles"
bl_label = "Print Inverse Obstacles"
bl_description = "Print all obstacles with the 'Inverse' option enabled to the system console."
@classmethod
def poll(cls, context):
return True
def execute(self, context):
print()
print("*** Inverse Obstacle Objects ***")
obstacle_objects = context.scene.flip_fluid.get_obstacle_objects()
for obj in obstacle_objects:
if not obj.flip_fluid.is_active:
continue
if obj.flip_fluid.obstacle.is_inversed:
is_hidden = obj.hide_viewport or obj.hide_get()
print(obj, "<hidden: " + str(is_hidden) + ">")
return {'FINISHED'}
class FlipFluidSupportSelectInverseObstacles(bpy.types.Operator):
bl_idname = "flip_fluid_operators.select_inverse_obstacles"
bl_label = "Select Inverse Obstacles"
bl_description = "Select all obstacles with the 'Inverse' option enabled. Will not select hidden objects"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
for obj in bpy.data.objects:
obj.select_set(False)
obstacle_objects = context.scene.flip_fluid.get_obstacle_objects()
for obj in obstacle_objects:
if not obj.flip_fluid.is_active:
continue
if obj.flip_fluid.obstacle.is_inversed:
obj.select_set(True)
if len(bpy.context.selected_objects) > 0:
vcu.set_active_object(bpy.context.selected_objects[0])
return {'FINISHED'}
class FlipFluidSupportInvertSelection(bpy.types.Operator):
bl_idname = "flip_fluid_operators.invert_selection"
bl_label = "Invert Selection"
bl_description = "Inverts selection of currently selected objects"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
bpy.ops.object.select_all(action='INVERT')
return {'FINISHED'}
class FlipFluidSupportIncrementAndSaveFile(bpy.types.Operator):
bl_idname = "flip_fluid_operators.increment_and_save_file"
bl_label = "Increment Version and Save Blend File"
bl_description = "Increment Blend filename version and save file"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
filepath = bpy.data.filepath
basename = os.path.basename(filepath)
extension = os.path.splitext(basename)[-1]
filename = basename[:-len(extension)]
base_filename = filename.rstrip(string.digits)
parent_path = os.path.dirname(filepath)
for i in range(1, 999):
num_str = str(i).zfill(3)
new_path = os.path.join(parent_path, base_filename + num_str + extension)
if os.path.isfile(new_path):
continue
bpy.ops.wm.save_as_mainfile(filepath=new_path)
break
return {'FINISHED'}
def register():
bpy.utils.register_class(FlipFluidSupportPrintSystemInfo)
bpy.utils.register_class(FlipFluidSupportStandardizeBlendFile)
bpy.utils.register_class(FlipFluidSupportDisplayOverlayStats)
bpy.utils.register_class(FlipFluidSupportSelectSimulationObjects)
bpy.utils.register_class(FlipFluidSupportSelectHiddenSimulationObjects)
bpy.utils.register_class(FlipFluidSupportPrintHiddenSimulationObjects)
bpy.utils.register_class(FlipFluidSupportSelectInverseObstacles)
bpy.utils.register_class(FlipFluidSupportPrintInverseObstacles)
bpy.utils.register_class(FlipFluidSupportInvertSelection)
bpy.utils.register_class(FlipFluidSupportIncrementAndSaveFile)
def unregister():
bpy.utils.unregister_class(FlipFluidSupportPrintSystemInfo)
bpy.utils.unregister_class(FlipFluidSupportStandardizeBlendFile)
bpy.utils.unregister_class(FlipFluidSupportDisplayOverlayStats)
bpy.utils.unregister_class(FlipFluidSupportSelectSimulationObjects)
bpy.utils.unregister_class(FlipFluidSupportSelectHiddenSimulationObjects)
bpy.utils.unregister_class(FlipFluidSupportPrintHiddenSimulationObjects)
bpy.utils.unregister_class(FlipFluidSupportSelectInverseObstacles)
bpy.utils.unregister_class(FlipFluidSupportPrintInverseObstacles)
bpy.utils.unregister_class(FlipFluidSupportInvertSelection)
bpy.utils.unregister_class(FlipFluidSupportIncrementAndSaveFile)
@@ -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
class FlipFluidMakeZeroGravity(bpy.types.Operator):
bl_idname = "flip_fluid_operators.make_zero_gravity"
bl_label = "Set to Zero Gravity"
bl_description = "Quickly switch to custom gravity mode set to zero gravity"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
if dprops is None:
return {'CANCELLED'}
dprops.world.gravity_type = 'GRAVITY_TYPE_CUSTOM'
dprops.world.gravity = (0.0, 0.0, 0.0)
return {'FINISHED'}
def register():
bpy.utils.register_class(FlipFluidMakeZeroGravity)
def unregister():
bpy.utils.unregister_class(FlipFluidMakeZeroGravity)