2025-12-01
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user