2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -0,0 +1,56 @@
# 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 = [
'flip_fluid_aabb',
'flip_fluid_cache',
'flip_fluid_material_library'
'flip_fluid_map',
'flip_fluid_geometry_export_object',
'flip_fluid_geometry_database',
'flip_fluid_geometry_exporter',
'flip_fluid_preset_stack',
]
for module_name in reloadable_modules:
if module_name in locals():
importlib.reload(locals()[module_name])
import bpy
from . import (
flip_fluid_aabb,
flip_fluid_cache,
flip_fluid_material_library,
flip_fluid_map,
flip_fluid_geometry_export_object,
flip_fluid_geometry_database,
flip_fluid_geometry_exporter,
flip_fluid_preset_stack,
)
def register():
flip_fluid_cache.register()
flip_fluid_material_library.register()
flip_fluid_preset_stack.register()
def unregister():
flip_fluid_cache.unregister()
flip_fluid_material_library.unregister()
flip_fluid_preset_stack.unregister()
@@ -0,0 +1,104 @@
# 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/>.
from ..utils import version_compatibility_utils as vcu
class AABB(object):
def __init__(self, x=0, y=0, z=0, xdim=0, ydim=0, zdim=0):
self.x = x
self.y = y
self.z = z
self.xdim = xdim
self.ydim = ydim
self.zdim = zdim
@classmethod
def from_blender_object(cls, obj):
if obj.type == 'MESH':
vertices = obj.data.vertices
elif obj.type == 'CURVE':
spline = obj.data.splines[0]
if spline.type == 'BEZIER':
vertices = spline.bezier_points
elif spline.type == 'NURBS':
vertices = spline.points
elif obj.type == 'EMPTY':
position = obj.matrix_world.translation
return cls(position[0], position[1], position[2], 0.0, 0.0, 0.0)
xmin, ymin, zmin = float('inf'), float('inf'), float('inf')
xmax, ymax, zmax = -float('inf'), -float('inf'), -float('inf')
for mv in vertices:
v = vcu.element_multiply(obj.matrix_world, mv.co)
xmin = min(v.x, xmin)
ymin = min(v.y, ymin)
zmin = min(v.z, zmin)
xmax = max(v.x, xmax)
ymax = max(v.y, ymax)
zmax = max(v.z, zmax)
xdim, ydim, zdim = xmax - xmin, ymax - ymin, zmax - zmin
return cls(xmin, ymin, zmin, xdim, ydim, zdim)
def is_empty(self, tol = 1e-6):
return self.xdim * self.ydim * self.zdim < tol
def contains(self, bbox):
return (bbox.x > self.x and bbox.y > self.y and bbox.z > self.z and
bbox.x + bbox.xdim < self.x + self.xdim and
bbox.y + bbox.ydim < self.y + self.ydim and
bbox.z + bbox.zdim < self.z + self.zdim)
def contains_point(self, p):
return (p[0] >= self.x and p[0] < self.x + self.xdim and
p[1] >= self.y and p[1] < self.y + self.ydim and
p[2] >= self.z and p[2] < self.z + self.zdim)
def expand(self, amount):
hw = 0.5 * amount
self.x -= hw
self.y -= hw
self.z -= hw
self.xdim += amount
self.ydim += amount
self.zdim += amount
return AABB(self.x - hw, self.y - hw, self.z - hw,
self.xdim + hw, self.ydim + hw, self.zdim + hw)
def intersection(self, bbox):
xmin = max(self.x, bbox.x)
ymin = max(self.y, bbox.y)
zmin = max(self.z, bbox.z)
xmax = min(self.x + self.xdim, bbox.x + bbox.xdim)
ymax = min(self.y + self.ydim, bbox.y + bbox.ydim)
zmax = min(self.z + self.zdim, bbox.z + bbox.zdim)
xdim = max(0.0, xmax - xmin)
ydim = max(0.0, ymax - ymin)
zdim = max(0.0, zmax - zmin)
return AABB(xmin, ymin, zmin, xdim, ydim, zdim)
def to_dict(self):
return {'x': self.x, 'y': self.y, 'z': self.z,
'xdim': self.xdim, 'ydim': self.ydim, 'zdim': self.zdim}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,413 @@
# 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, enum
from mathutils import Vector, Matrix, Quaternion, Euler, Color
from ..ffengine import TriangleMesh
from ..utils import export_utils
from ..utils import version_compatibility_utils as vcu
from ..utils import cache_utils
###########################################################################
### Geometry Helpers
###########################################################################
def find_fcurve(id_data, path, index=0):
anim_data = id_data.animation_data
for fcurve in anim_data.action.fcurves:
if fcurve.data_path == path and fcurve.array_index == index:
return fcurve
def get_vector3_at_frame(obj, path, frame_id):
xcurve = find_fcurve(obj, path, 0)
ycurve = find_fcurve(obj, path, 1)
zcurve = find_fcurve(obj, path, 2)
x = xcurve.evaluate(frame_id) if xcurve else getattr(obj, path)[0]
y = ycurve.evaluate(frame_id) if ycurve else getattr(obj, path)[1]
z = zcurve.evaluate(frame_id) if zcurve else getattr(obj, path)[2]
return (x, y, z)
def get_vector4_at_frame(obj, path, frame_id):
wcurve = find_fcurve(obj, path, 0)
xcurve = find_fcurve(obj, path, 1)
ycurve = find_fcurve(obj, path, 2)
zcurve = find_fcurve(obj, path, 3)
w = wcurve.evaluate(frame_id) if wcurve else getattr(obj, path)[0]
x = xcurve.evaluate(frame_id) if xcurve else getattr(obj, path)[1]
y = ycurve.evaluate(frame_id) if ycurve else getattr(obj, path)[2]
z = zcurve.evaluate(frame_id) if zcurve else getattr(obj, path)[3]
return (w, x, y, z)
def get_rotation_mode_at_frame(obj, frame_id):
mode_curve = find_fcurve(obj, "rotation_mode")
mode = mode_curve.evaluate(frame_id) if mode_curve else getattr(obj, "rotation_mode")
if isinstance(mode, float):
# Handles case where rotation mode is keyframed - enum returns a float instead of a string
enum = int(mode)
if mode == 0:
mode = 'QUATERNION'
elif mode == 1:
mode = 'XYZ'
elif mode == 2:
mode = 'XZY'
elif mode == 3:
mode = 'YXZ'
elif mode == 4:
mode = 'YZX'
elif mode == 5:
mode = 'ZXY'
elif mode == 6:
mode = 'ZYX'
elif mode == 7:
mode = 'AXIS_ANGLE'
return mode
def get_matrix_world_at_frame(obj, frame_id):
rotation_mode = get_rotation_mode_at_frame(obj, frame_id)
if rotation_mode == 'AXIS_ANGLE':
axis_angle = get_vector4_at_frame(obj, "rotation_axis_angle", frame_id)
angle = axis_angle[0]
axis = Vector((axis_angle[1], axis_angle[2], axis_angle[3]))
rotation_matrix = Matrix.Rotation(angle, 4, axis)
elif rotation_mode == 'QUATERNION':
rotation_quat = get_vector4_at_frame(obj, "rotation_quaternion", frame_id)
quaternion = Quaternion(rotation_quat)
rotation_matrix = quaternion.to_euler().to_matrix().to_4x4()
else:
rotation = get_vector3_at_frame(obj, "rotation_euler", frame_id)
euler_rotation = Euler(rotation, rotation_mode)
rotation_matrix = euler_rotation.to_matrix().to_4x4()
location = get_vector3_at_frame(obj, "location", frame_id)
location_matrix = Matrix.Translation(location).to_4x4()
scale = get_vector3_at_frame(obj, "scale", frame_id)
scale_matrix = Matrix.Identity(4)
scale_matrix[0][0] = scale[0]
scale_matrix[1][1] = scale[1]
scale_matrix[2][2] = scale[2]
return vcu.element_multiply(vcu.element_multiply(location_matrix, rotation_matrix), scale_matrix)
def get_mesh_centroid(obj, apply_transforms=True):
if apply_transforms:
tmesh = vcu.object_to_triangle_mesh(obj, obj.matrix_world)
else:
tmesh = vcu.object_to_triangle_mesh(obj)
num_vertices = len(tmesh.vertices) // 3
if num_vertices == 0:
return (0.0, 0.0, 0.0)
xacc, yacc, zacc = 0.0, 0.0, 0.0
for vidx in range(0, num_vertices):
xacc += tmesh.vertices[3 * vidx + 0]
yacc += tmesh.vertices[3 * vidx + 1]
zacc += tmesh.vertices[3 * vidx + 2]
xacc /= num_vertices
yacc /= num_vertices
zacc /= num_vertices
return (xacc, yacc, zacc)
# Method adapted from: https://blender.stackexchange.com/a/93441
def curve_to_triangle_mesh(bl_curve_object, apply_transforms=True):
if not apply_transforms:
orig_location = Vector(bl_curve_object.location)
if bl_curve_object.rotation_mode == 'QUATERNION':
orig_rotation = Vector(bl_curve_object.rotation_quaternion)
bl_curve_object.rotation_quaternion = (0, 0, 0, 0)
elif bl_curve_object.rotation_mode == 'AXIS_ANGLE':
orig_rotation = Vector(bl_curve_object.rotation_axis_angle)
bl_curve_object.rotation_axis_angle = (0, 0, 0, 0)
else:
orig_rotation = Vector(bl_curve_object.rotation_euler)
bl_curve_object.rotation_euler = (0, 0, 0)
orig_scale = Vector(bl_curve_object.scale)
bl_curve_object.location = (0, 0, 0)
bl_curve_object.scale = (1, 1, 1)
instance_obj = bpy.data.objects.new("Empty", None)
vcu.link_object_to_master_scene(instance_obj)
follow_path_constraint = instance_obj.constraints.new(type='FOLLOW_PATH')
follow_path_constraint.target = bl_curve_object
follow_path_constraint.use_fixed_location = True
spline = bl_curve_object.data.splines[0]
if spline.type == 'BEZIER':
num_curve_points = len(spline.bezier_points)
elif spline.type == 'NURBS' or spline.type == 'POLY':
num_curve_points = len(spline.points)
extra_vertex = 0 if spline.use_cyclic_u else 1
resolution = spline.resolution_u + 1
num_vertices = (num_curve_points + extra_vertex) * resolution
instances = [instance_obj]
for i in range(1, num_vertices + extra_vertex):
temp_instance = instance_obj.copy()
temp_instance_constraint = temp_instance.constraints[0]
temp_instance_constraint.offset_factor = i / num_vertices
vcu.link_object_to_master_scene(temp_instance)
instances.append(temp_instance)
vcu.depsgraph_update()
tmesh = TriangleMesh()
for instance in instances:
instance_constraint = instance.constraints[0]
vertex = instance.matrix_world.translation
tmesh.vertices.append(vertex[0])
tmesh.vertices.append(vertex[1])
tmesh.vertices.append(vertex[2])
instance.constraints.remove(instance_constraint)
vcu.delete_object(instance)
if not apply_transforms:
bl_curve_object.location = orig_location
if bl_curve_object.rotation_mode == 'QUATERNION':
bl_curve_object.rotation_quaternion = orig_rotation
elif bl_curve_object.rotation_mode == 'AXIS_ANGLE':
bl_curve_object.rotation_axis_angle = orig_rotation
else:
bl_curve_object.rotation_euler = orig_rotation
bl_curve_object.scale = orig_scale
return tmesh
###########################################################################
### Geometry Export Object
###########################################################################
class MotionExportType(enum.Enum):
STATIC = 0
KEYFRAMED = 1
ANIMATED = 2
class GeometryExportType(enum.Enum):
MESH = 0
VERTICES = 1
CENTROID = 2
AXIS = 3
CURVE = 4
class GeometryExportObject():
def __init__(self, name):
self.name = name
self.name_slug = cache_utils.string_to_cache_slug(name)
self.motion_export_type = self._initialize_motion_export_type()
self.geometry_export_types = []
self.skip_reexport = False
self.disable_changing_topology_warning = False
self.frame_start = 0
self.frame_end = 0
self.exported_frames = {}
self._object_id = -1
def get_blender_object(self):
return bpy.data.objects[self.name]
def is_static(self):
return self.motion_export_type == MotionExportType.STATIC
def is_keyframed(self):
return self.motion_export_type == MotionExportType.KEYFRAMED
def is_animated(self):
return self.motion_export_type == MotionExportType.ANIMATED
def is_dynamic(self):
return self.is_keyframed() or self.is_animated()
def motion_export_type_to_string(self):
if self.is_static():
return 'STATIC'
elif self.is_keyframed():
return 'KEYFRAMED'
elif self.is_animated():
return 'ANIMATED'
def is_exporting_mesh(self):
return GeometryExportType.MESH in self.geometry_export_types
def is_exporting_vertices(self):
return GeometryExportType.VERTICES in self.geometry_export_types
def is_exporting_centroid(self):
return GeometryExportType.CENTROID in self.geometry_export_types
def is_exporting_axis(self):
return GeometryExportType.AXIS in self.geometry_export_types
def is_exporting_curve(self):
return GeometryExportType.CURVE in self.geometry_export_types
def set_export_frame_range(self, frame_start, frame_end):
if self.is_static():
raise Exception("Frame range must only be set for dynamic export objects.")
self.frame_start = frame_start
self.frame_end = frame_end
def set_motion_export_type(self, motion_export_type_enum):
if not isinstance(motion_export_type_enum, MotionExportType):
raise TypeError("Value must MotionExportType enum.")
self.motion_export_type = motion_export_type_enum
def add_geometry_export_type(self, geometry_export_type):
if not isinstance(geometry_export_type, GeometryExportType):
raise TypeError("Value must GeometryExportType enum.")
if geometry_export_type not in self.geometry_export_types:
self.geometry_export_types.append(geometry_export_type)
def clear_geometry_export_types(self):
self.geometry_export_types = []
def merge(self, other_export_object):
if not isinstance(other_export_object, GeometryExportObject):
raise TypeError("Value must GeometryExportObject enum.")
if self.name != other_export_object.name:
raise Exception("Export objects must have same name to be merged.")
if other_export_object.motion_export_type.value > self.motion_export_type.value:
self.set_motion_export_type(other_export_object.motion_export_type)
self.frame_start = other_export_object.frame_start
self.frame_end = other_export_object.frame_end
for geometry_type in other_export_object.geometry_export_types:
self.add_geometry_export_type(geometry_type)
def set_object_id(self, database_object_id):
self._database_object_id = database_object_id
def get_object_id(self):
return self._database_object_id
def exported_frame_exists(self, geometry_export_type, frame_id):
if not geometry_export_type in self.geometry_export_types:
return False
if not geometry_export_type in self.exported_frames:
return False
if not frame_id in self.exported_frames[geometry_export_type]:
return False
return self.exported_frames[geometry_export_type][frame_id]
def get_mesh_bobj(self, apply_transforms=True):
bl_object = self.get_blender_object()
if apply_transforms:
tmesh = vcu.object_to_triangle_mesh(bl_object, bl_object.matrix_world)
else:
tmesh = vcu.object_to_triangle_mesh(bl_object)
return tmesh.to_bobj()
def get_centroid(self, apply_transforms=True):
bl_object = self.get_blender_object()
if bl_object.type == 'MESH':
return get_mesh_centroid(bl_object, apply_transforms=apply_transforms)
else:
vect = bl_object.matrix_world.translation
return (vect.x, vect.y, vect.z)
def get_centroid_at_frame(self, frame_id):
bl_object = self.get_blender_object()
matrix_world = self.get_matrix_world_at_frame(frame_id)
if bl_object.type == 'MESH':
c = get_mesh_centroid(bl_object, apply_transforms=False)
centroid = Vector((c[0], c[1], c[2]))
vect = vcu.element_multiply(matrix_world, centroid)
else:
vect = matrix_world.translation
return (vect.x, vect.y, vect.z)
def get_local_axis(self):
bl_object = self.get_blender_object()
mat = bl_object.matrix_world
X = Vector((mat[0][0], mat[1][0], mat[2][0])).normalized()
Y = Vector((mat[0][1], mat[1][1], mat[2][1])).normalized()
Z = Vector((mat[0][2], mat[1][2], mat[2][2])).normalized()
return (X.x, X.y, X.z), (Y.x, Y.y, Y.z), (Z.x, Z.y, Z.z)
def get_local_axis_at_frame(self, frame_id):
bl_object = self.get_blender_object()
mat = self.get_matrix_world_at_frame(frame_id)
X = Vector((mat[0][0], mat[1][0], mat[2][0])).normalized()
Y = Vector((mat[0][1], mat[1][1], mat[2][1])).normalized()
Z = Vector((mat[0][2], mat[1][2], mat[2][2])).normalized()
return (X.x, X.y, X.z), (Y.x, Y.y, Y.z), (Z.x, Z.y, Z.z)
def get_matrix_world_at_frame(self, frame_id):
bl_object = self.get_blender_object()
return get_matrix_world_at_frame(bl_object, frame_id)
def get_curve_bobj(self, apply_transforms=True):
bl_object = self.get_blender_object()
tmesh = curve_to_triangle_mesh(bl_object, apply_transforms)
return tmesh.to_bobj()
def get_bobj_vertex_triangle_count(self, bobj_data):
tmesh = TriangleMesh.from_bobj(bobj_data)
return len(tmesh.vertices) // 3, len(tmesh.triangles) // 3
def _initialize_motion_export_type(self):
obj = self.get_blender_object()
props = obj.flip_fluid.get_property_group()
if hasattr(props, 'export_animated_mesh') and props.export_animated_mesh:
return MotionExportType.ANIMATED
if export_utils.is_object_keyframe_animated(obj):
return MotionExportType.KEYFRAMED
return MotionExportType.STATIC
@@ -0,0 +1,511 @@
# 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, copy
from datetime import datetime
from . import flip_fluid_cache
from .flip_fluid_geometry_export_object import GeometryExportObject, MotionExportType, GeometryExportType
from .flip_fluid_geometry_database import GeometryDatabase
from ..utils import version_compatibility_utils as vcu
from ..utils import cache_utils
class WorkQueueItem():
def __init__(self):
self.geometry_export_object = None
self.geometry_export_type = None
self.frame = 0
self.apply_transforms = True
class GeometryExportManager():
def __init__(self, export_directory):
self.geometry_export_objects = []
self._database_filename = "export_data.sqlite3"
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
self._database_filepath = dprops.cache.get_geometry_database_abspath(export_directory, self._database_filename)
self._is_linked_geometry_database = dprops.cache.is_linked_geometry_directory()
self._geometry_database = GeometryDatabase(self._database_filepath, clear_database=False)
self._geometry_export_objects_dict = {}
self._is_initialized = False
self._work_queue = []
self._static_queue_size = 0
self._keyframed_queue_size = 0
self._animated_queue_size = 0
self._num_static_processed = 0
self._num_keyframed_processed = 0
self._num_animated_processed = 0
self._export_stage_string = ""
self._export_stage_progress = 0.0
self._is_error = False
self._error_message = ""
def add_geometry_export_object(self, export_object):
if export_object.name_slug in self._geometry_export_objects_dict:
existing_object = self._geometry_export_objects_dict[export_object.name_slug]
existing_object.merge(export_object)
else:
self.geometry_export_objects.append(export_object)
self._geometry_export_objects_dict[export_object.name_slug] = export_object
def get_geometry_export_object(self, name):
slug = cache_utils.string_to_cache_slug(name)
if slug in self._geometry_export_objects_dict:
return self._geometry_export_objects_dict[slug]
return None
def get_geometry_export_objects_by_export_type(self, export_type):
if isinstance(export_type, MotionExportType):
return [obj for obj in self.geometry_export_objects if obj.motion_export_type == export_type]
elif isinstance(export_type, GeometryExportType):
return [obj for obj in self.geometry_export_objects if export_type in obj.geometry_export_types]
else:
raise TypeError("Value must MotionExportType or GeometryExportType enum.")
def initialize(self):
if self._is_initialized:
raise Exception("GeometryExportManager already initialized.")
for obj in self.geometry_export_objects:
obj.geometry_export_types.sort(key=lambda x: x.value)
self._geometry_database.open()
try:
self._delete_geometry_export_objects_from_database()
self._add_geometry_export_objects_to_database()
self._initialize_geometry_export_object_ids()
self._clean_unused_objects_from_database()
self._initialize_frame_ranges()
self._initialize_work_queues()
self._geometry_database.commit()
except Exception as e:
self._geometry_database.close()
raise e
self._geometry_database.close()
self._is_initialized = True
def update_export(self, step_time):
if not self._work_queue:
return True
self._geometry_database.open()
self._geometry_database.begin()
try:
start_time = datetime.now()
while self._work_queue:
work_item = self._work_queue.pop()
self._process_work_item(work_item)
if self._is_error:
self._geometry_database.close()
return True
if self._get_elapsed_time(start_time) >= step_time:
break
self._set_export_state(work_item)
except Exception as e:
self._geometry_database.close()
raise e
self._geometry_database.commit()
self._geometry_database.close()
filesize = self._geometry_database.get_filesize()
num_processed = (self._total_queue_size - len(self._work_queue))
output_str = "Exporting... " + str(num_processed) + " / " + str(self._total_queue_size) + " objects "
output_str += "\t(Database size: " + str(filesize) + ")"
print(output_str)
return not self._work_queue
def get_export_progress(self):
return self._export_stage_progress
def get_export_stage(self):
return self._export_stage_string
def is_error(self):
return self._is_error
def get_error_message(self):
return self._error_message
def _initialize_work_queues(self):
static_queue = self._generate_static_work_queue()
keyframed_queue = self._generate_keyframed_work_queue()
keyframed_static_basis_queue = self._generate_keyframed_static_basis_work_queue()
animated_queue = self._generate_animated_work_queue()
total_queue = animated_queue + keyframed_queue + keyframed_static_basis_queue + static_queue
self._work_queue = total_queue
self._static_queue_size = len(static_queue + keyframed_static_basis_queue)
self._keyframed_queue_size = len(keyframed_queue)
self._animated_queue_size = len(animated_queue)
self._total_queue_size = len(total_queue)
def _generate_static_work_queue(self):
static_objects = self.get_geometry_export_objects_by_export_type(MotionExportType.STATIC)
work_queue = []
for obj in static_objects:
for geotype in obj.geometry_export_types:
if obj.skip_reexport:
export_data_exists = self._geometry_database.static_geometry_exists(obj.get_object_id(), geotype)
if export_data_exists:
continue
w = WorkQueueItem()
w.geometry_export_object = obj
w.geometry_export_type = geotype
work_queue.append(w)
work_queue.reverse()
return work_queue
# Keyframed objects also need to export a static version of the geometry to apply
# transforms upon
def _generate_keyframed_static_basis_work_queue(self):
keyframed_objects = self.get_geometry_export_objects_by_export_type(MotionExportType.KEYFRAMED)
work_queue = []
for obj in keyframed_objects:
obj_copy = copy.copy(obj)
obj_copy.motion_export_type = MotionExportType.STATIC
for geotype in obj_copy.geometry_export_types:
if obj_copy.skip_reexport:
export_data_exists = self._geometry_database.static_geometry_exists(obj_copy.get_object_id(), geotype)
if export_data_exists:
continue
w = WorkQueueItem()
w.geometry_export_object = obj_copy
w.geometry_export_type = geotype
w.apply_transforms = False
work_queue.append(w)
work_queue.reverse()
return work_queue
def _generate_dynamic_work_queue_for_frame(self, export_objects, frameno):
work_queue = []
for obj in export_objects:
for geotype in obj.geometry_export_types:
if obj.skip_reexport and obj.exported_frame_exists(geotype, frameno):
continue
w = WorkQueueItem()
w.geometry_export_object = obj
w.geometry_export_type = geotype
w.frame = frameno
work_queue.append(w)
return work_queue
def _get_min_max_frame_range(self, export_object_list):
min_obj = min(export_object_list, key=lambda x: x.frame_start)
max_obj = max(export_object_list, key=lambda x: x.frame_end)
return min_obj.frame_start, max_obj.frame_end
def _generate_keyframed_work_queue(self):
work_queue = []
keyframed_objects = self.get_geometry_export_objects_by_export_type(MotionExportType.KEYFRAMED)
if not keyframed_objects:
return work_queue
frame_start, frame_end = self._get_min_max_frame_range(keyframed_objects)
for frameno in range(frame_start, frame_end + 1):
work_queue += self._generate_dynamic_work_queue_for_frame(keyframed_objects, frameno)
work_queue.reverse()
return work_queue
def _generate_animated_work_queue(self):
work_queue = []
animated_objects = self.get_geometry_export_objects_by_export_type(MotionExportType.ANIMATED)
if not animated_objects:
return work_queue
frame_start, frame_end = self._get_min_max_frame_range(animated_objects)
for frameno in range(frame_start, frame_end + 1):
work_queue += self._generate_dynamic_work_queue_for_frame(animated_objects, frameno)
work_queue.reverse()
return work_queue
def _get_elapsed_time(self, start_time):
dt = datetime.now() - start_time
return dt.days * 86400 + dt.seconds + dt.microseconds / 1000000.0
def _set_export_state(self, work_item):
motion_type = work_item.geometry_export_object.motion_export_type
if motion_type == MotionExportType.STATIC:
self._export_stage_string = "STATIC"
self._export_stage_progress = self._num_static_processed / self._static_queue_size
elif motion_type == MotionExportType.KEYFRAMED:
self._export_stage_string = "KEYFRAMED"
self._export_stage_progress = self._num_keyframed_processed / self._keyframed_queue_size
elif motion_type == MotionExportType.ANIMATED:
self._export_stage_string = "ANIMATED"
self._export_stage_progress = self._num_animated_processed / self._animated_queue_size
def _set_error(self, errmsg):
self._is_error = True
self._error_message = errmsg
###########################################################################
### Database Operations
###########################################################################
def _delete_geometry_export_objects_from_database(self):
for obj in self.geometry_export_objects:
if not obj.skip_reexport and self._geometry_database.object_exists(obj):
self._geometry_database.delete_object_by_slug(obj.name_slug)
def _add_geometry_export_objects_to_database(self):
for obj in self.geometry_export_objects:
if not self._geometry_database.object_exists(obj):
self._geometry_database.add_object(obj)
def _initialize_geometry_export_object_ids(self):
for obj in self.geometry_export_objects:
obj.set_object_id(self._geometry_database.get_object_id(obj))
def _clean_unused_objects_from_database(self):
if self._is_linked_geometry_database:
return
all_objects = self._geometry_database.get_all_objects()
database_slugs = [row[2] for row in all_objects]
current_slugs = [obj.name_slug for obj in self.geometry_export_objects]
for slug in database_slugs:
if slug not in current_slugs:
self._geometry_database.delete_object_by_slug(slug)
def _initialize_geometry_export_object_frame_range(self, obj):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
frame_start, frame_end = dprops.simulation.get_frame_range()
obj.set_export_frame_range(frame_start, frame_end)
if not obj.skip_reexport:
return
ident = obj.get_object_id()
mtype = obj.motion_export_type
frame_data_dict = {}
for geotype in obj.geometry_export_types:
frame_data_dict[geotype] = {}
frame_list = self._geometry_database.get_dynamic_geometry_exported_frames(ident, mtype, geotype)
for frame_id in range(frame_start, frame_end + 1):
frame_data_dict[geotype][frame_id] = False
for frame_id in frame_list:
frame_data_dict[geotype][frame_id] = True
obj.exported_frames = frame_data_dict
def _initialize_frame_ranges(self):
for obj in self.geometry_export_objects:
if not obj.is_dynamic():
continue
self._initialize_geometry_export_object_frame_range(obj)
###########################################################################
### Process Geometry
###########################################################################
def _update_blender_frame(self, frameno):
if bpy.context.scene.frame_current == frameno:
return
flip_fluid_cache.DISABLE_MESH_CACHE_LOAD = True
bpy.context.scene.frame_set(frameno)
flip_fluid_cache.DISABLE_MESH_CACHE_LOAD = False
def _process_mesh_object(self, work_item):
motion_type = work_item.geometry_export_object.motion_export_type
if motion_type == MotionExportType.STATIC:
object_id = work_item.geometry_export_object.get_object_id()
bobj_data = work_item.geometry_export_object.get_mesh_bobj(apply_transforms=work_item.apply_transforms)
self._geometry_database.add_mesh_static(object_id, bobj_data)
elif motion_type == MotionExportType.KEYFRAMED:
object_id = work_item.geometry_export_object.get_object_id()
frame_id = work_item.frame
matrix_world = work_item.geometry_export_object.get_matrix_world_at_frame(frame_id)
self._geometry_database.add_mesh_keyframed(object_id, frame_id, matrix_world)
elif motion_type == MotionExportType.ANIMATED:
frame_id = work_item.frame
self._update_blender_frame(frame_id)
name_slug = work_item.geometry_export_object.name_slug
disable_warning = work_item.geometry_export_object.disable_changing_topology_warning
bobj_data = work_item.geometry_export_object.get_mesh_bobj()
if not disable_warning:
current_frame_bytes = len(bobj_data)
previous_frame_bytes = self._geometry_database.get_mesh_animated_blob_length(name_slug, frame_id - 1)
if previous_frame_bytes is not None and current_frame_bytes != previous_frame_bytes:
current_bobj_data = bobj_data
previous_bobj_data = previous_frame_bytes = self._geometry_database.get_mesh_animated(name_slug, frame_id - 1)
current_vcount, current_tcount = work_item.geometry_export_object.get_bobj_vertex_triangle_count(current_bobj_data)
previous_vcount, previous_tcount = work_item.geometry_export_object.get_bobj_vertex_triangle_count(previous_bobj_data)
bl_object = bpy.data.objects.get(work_item.geometry_export_object.name)
error_reason = "Unknown"
if bl_object is not None:
if bl_object.flip_fluid.is_obstacle():
error_reason = "Obstacle object require mesh velocities to be computed for fluid interaction."
elif bl_object.flip_fluid.is_inflow():
error_reason = "Inflow object 'Add Object Velocity to Inflow' option require mesh velocities to be computed for this feature."
elif bl_object.flip_fluid.is_fluid():
error_reason = "Fluid object 'Add Object Velocity to Fluid' option require mesh velocities to be computed for this feature."
errmsg = ("Warning: unable to export animated mesh '" + work_item.geometry_export_object.name +
"'. Animated meshes must have the same number of " +
"vertices/triangles for each frame and must not change topology\nif the mesh velocity is required to be computed correctly." +
"\nError Reason: " + error_reason +
"\n\nFrame " + str(frame_id - 1) + ": " + str(previous_vcount) + " vertices, " + str(previous_tcount) + " triangles"
"\nFrame " + str(frame_id) + ": " + str(current_vcount)) + " vertices, " + str(current_tcount) + " triangles"
errmsg += ("\n\nDisable this warning in the Advanced Settings panel. Warning: " +
"mesh velocity data will not be computed for meshes with changing topology.")
self._set_error(errmsg)
object_id = work_item.geometry_export_object.get_object_id()
self._geometry_database.add_mesh_animated(object_id, frame_id, bobj_data)
def _process_centroid_object(self, work_item):
motion_type = work_item.geometry_export_object.motion_export_type
if motion_type == MotionExportType.STATIC:
object_id = work_item.geometry_export_object.get_object_id()
centroid = work_item.geometry_export_object.get_centroid(apply_transforms=work_item.apply_transforms)
self._geometry_database.add_centroid_static(object_id, centroid)
elif motion_type == MotionExportType.KEYFRAMED:
object_id = work_item.geometry_export_object.get_object_id()
frame_id = work_item.frame
centroid = work_item.geometry_export_object.get_centroid_at_frame(frame_id)
self._geometry_database.add_centroid_keyframed(object_id, frame_id, centroid)
elif motion_type == MotionExportType.ANIMATED:
frame_id = work_item.frame
self._update_blender_frame(frame_id)
object_id = work_item.geometry_export_object.get_object_id()
centroid = work_item.geometry_export_object.get_centroid()
self._geometry_database.add_centroid_animated(object_id, frame_id, centroid)
def _process_axis_object(self, work_item):
motion_type = work_item.geometry_export_object.motion_export_type
if motion_type == MotionExportType.STATIC:
object_id = work_item.geometry_export_object.get_object_id()
local_x, local_y, local_z = work_item.geometry_export_object.get_local_axis()
self._geometry_database.add_axis_static(object_id, local_x, local_y, local_z)
elif motion_type == MotionExportType.KEYFRAMED:
object_id = work_item.geometry_export_object.get_object_id()
frame_id = work_item.frame
local_x, local_y, local_z = work_item.geometry_export_object.get_local_axis_at_frame(frame_id)
self._geometry_database.add_axis_keyframed(object_id, frame_id, local_x, local_y, local_z)
elif motion_type == MotionExportType.ANIMATED:
frame_id = work_item.frame
self._update_blender_frame(frame_id)
object_id = work_item.geometry_export_object.get_object_id()
local_x, local_y, local_z = work_item.geometry_export_object.get_local_axis()
self._geometry_database.add_axis_animated(object_id, frame_id, local_x, local_y, local_z)
def _process_curve_object(self, work_item):
motion_type = work_item.geometry_export_object.motion_export_type
if motion_type == MotionExportType.STATIC:
object_id = work_item.geometry_export_object.get_object_id()
bobj_data = work_item.geometry_export_object.get_curve_bobj(apply_transforms=work_item.apply_transforms)
self._geometry_database.add_curve_static(object_id, bobj_data)
elif motion_type == MotionExportType.KEYFRAMED:
object_id = work_item.geometry_export_object.get_object_id()
frame_id = work_item.frame
matrix_world = work_item.geometry_export_object.get_matrix_world_at_frame(frame_id)
self._geometry_database.add_curve_keyframed(object_id, frame_id, matrix_world)
elif motion_type == MotionExportType.ANIMATED:
frame_id = work_item.frame
self._update_blender_frame(frame_id)
object_id = work_item.geometry_export_object.get_object_id()
bobj_data = work_item.geometry_export_object.get_curve_bobj()
self._geometry_database.add_curve_animated(object_id, frame_id, bobj_data)
def _process_work_item(self, work_item):
export_type = work_item.geometry_export_type
if export_type == GeometryExportType.MESH:
self._process_mesh_object(work_item)
elif export_type == GeometryExportType.VERTICES:
pass
elif export_type == GeometryExportType.CENTROID:
self._process_centroid_object(work_item)
elif export_type == GeometryExportType.AXIS:
self._process_axis_object(work_item)
elif export_type == GeometryExportType.CURVE:
self._process_curve_object(work_item)
motion_type = work_item.geometry_export_object.motion_export_type
if motion_type == MotionExportType.STATIC:
self._num_static_processed += 1
elif motion_type == MotionExportType.KEYFRAMED:
self._num_keyframed_processed += 1
elif motion_type == MotionExportType.ANIMATED:
self._num_animated_processed += 1
@@ -0,0 +1,55 @@
# 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/>.
class Map(dict):
def __init__(self, dict_data):
super(Map, self).__init__(dict_data)
for k, v in dict_data.items():
if isinstance(v, dict):
self[k] = Map(v)
elif isinstance(v, list):
new_list = []
for list_element in v:
if isinstance(list_element, dict):
new_list.append(Map(list_element))
else:
new_list.append(list_element)
self[k] = new_list
else:
self[k] = v
def __getattr__(self, attr):
return self.get(attr)
def __setattr__(self, key, value):
self.__setitem__(key, value)
def __setitem__(self, key, value):
super(Map, self).__setitem__(key, value)
self.__dict__.update({key: value})
def __delattr__(self, item):
self.__delitem__(item)
def __delitem__(self, key):
super(Map, self).__delitem__(key)
del self.__dict__[key]
@@ -0,0 +1,341 @@
# 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, hashlib, bpy.utils.previews
from bpy.props import (
BoolProperty,
IntProperty,
FloatProperty,
EnumProperty,
StringProperty,
PointerProperty,
CollectionProperty
)
from .. import types
from ..utils import version_compatibility_utils as vcu
class FLIPFluidMaterialLibraryMaterial(bpy.types.PropertyGroup):
conv = vcu.convert_attribute_to_28
name = StringProperty(default=""); exec(conv("name"))
description = StringProperty(default=""); exec(conv("description"))
path = StringProperty(default=""); exec(conv("path"))
type = EnumProperty(items=types.material_types, default="MATERIAL_TYPE_SURFACE"); exec(conv("type"))
icon_id = IntProperty(default=-1); exec(conv("icon_id"))
imported_icon_id = IntProperty(default=-1); exec(conv("imported_icon_id"))
def get_ui_enum(self):
library = bpy.context.scene.flip_fluid_material_library
is_imported, imported_name = library.is_material_imported(self.name)
if is_imported:
display_name = imported_name
icon_id = self.imported_icon_id
else:
display_name = self.name
icon_id = self.icon_id
return (self.name, display_name, self.description, icon_id, self._get_hash())
def _get_hash(self):
# Append text when generating library material hash so that different
# hash values are created between library and non-library materials with
# the same name
hash_string = self.name + "flip_fluid_material_library"
return int(hashlib.sha1(hash_string.encode('utf-8')).hexdigest(), 16) % int(1e6)
class FLIPFluidMaterialLibrary(bpy.types.PropertyGroup):
conv = vcu.convert_attribute_to_28
library_path = StringProperty(default=""); exec(conv("library_path"))
material_list = CollectionProperty(type=FLIPFluidMaterialLibraryMaterial); exec(conv("material_list"))
@classmethod
def register(cls):
cls.material_icons = bpy.utils.previews.new()
@classmethod
def unregister(cls):
bpy.utils.previews.remove(cls.material_icons)
def load_post(self):
# Data block id (pointer) won't be consistent between saves so
# need reinitialize values of imported material library materials
for m in bpy.data.materials:
if m.flip_fluid_material_library.is_library_material:
m.flip_fluid_material_library.reinitialize_data_block_id(m)
def scene_update_post(self, scene):
for m in bpy.data.materials:
if not m.flip_fluid_material_library.is_library_material:
continue
if not m.flip_fluid_material_library.is_original_data_block(m):
m.flip_fluid_material_library.deactivate()
def initialize(self, library_path):
self.library_path = library_path
self._initialize_library_material_list()
self._initialize_preview_icons()
def check_icons_initialized(self):
# Icons will be cleared if Blender scripts are reloaded so
# this method can check and re-initialize icons if needed
if len(self.material_icons) > 0:
return
self._initialize_preview_icons()
def import_material(self, material_name):
if not self.is_material_in_library(material_name):
return material_name
is_imported, imported_name = self.is_material_imported(material_name)
if is_imported:
return imported_name
mdata = self._get_material_data_from_name(material_name)
with bpy.data.libraries.load(mdata.path) as (data_from, data_to):
for material in data_from.materials:
if material == material_name:
data_to.materials.append(material)
break
material_object = data_to.materials[0]
material_object.flip_fluid_material_library.activate(material_object, material_name)
return material_object.name
def get_import_material_copy_name(self, name):
prefix = "FF "
if name.startswith(prefix) and len(name) > len(prefix):
name = name[len(prefix):]
name = name.split('.')[0]
if not bpy.data.materials.get(name):
return name
duplicates = []
for m in bpy.data.materials:
if m.name == name:
duplicates.append(0)
continue
suffix = m.name.rsplit('.', 1)[-1]
if m.name.startswith(name) and len(m.name) == len(name) + 4 and suffix.isdigit():
duplicates.append(int(suffix))
if not duplicates:
return name
for i in range(1, 999):
if not i in duplicates:
return name + "." + str(i).zfill(3)
return name
def import_material_copy(self, material_name):
mdata = self._get_material_data_from_name(material_name)
with bpy.data.libraries.load(mdata.path) as (data_from, data_to):
for material in data_from.materials:
if material == material_name:
data_to.materials.append(material)
break
material_object = data_to.materials[0]
material_object.name = self.get_import_material_copy_name(material_object.name)
return material_object.name
def get_imported_material(self, library_name):
for m in bpy.data.materials:
libprops = m.flip_fluid_material_library
if libprops.is_library_material and libprops.library_name == library_name:
return m.name
return None
def is_material_imported(self, library_name):
for m in bpy.data.materials:
libprops = m.flip_fluid_material_library
if libprops.is_library_material and libprops.library_name == library_name:
return True, m.name
return False, ""
def is_material_in_library(self, material_name):
for mdata in self.material_list:
if mdata.name == material_name:
return True
return False
def _initialize_library_material_list(self):
self.material_list.clear()
extension = "blend"
subdirs = ["surface", "whitewater", "all"]
for subdir in subdirs:
subdir_path = os.path.join(self.library_path, subdir)
subdir_files = glob.glob(subdir_path + "/*." + extension)
for f in subdir_files:
if subdir == "surface":
mtype = 'MATERIAL_TYPE_SURFACE'
elif subdir == "whitewater":
mtype = 'MATERIAL_TYPE_WHITEWATER'
elif subdir == "all":
mtype = 'MATERIAL_TYPE_ALL'
description_path = f[:-len(extension)] + "description"
description_text = ""
if os.path.isfile(description_path):
with open(description_path, 'r', encoding='utf-8') as description_file:
description_text = description_file.read()
new_material = self.material_list.add()
new_material.name = os.path.splitext(os.path.basename(f))[0]
new_material.description = str(description_text)
new_material.path = f
new_material.type = mtype
new_material.icon_id = -1 # Will be set after icons are initialized
def _calculate_material_library_hash(self):
if len(self.material_list) == 0:
return "0"
SHAhash = hashlib.md5()
for m in self.material_list:
with open(m.path, "rb") as f:
bytes = f.read()
SHAhash.update(hashlib.md5(bytes).hexdigest().encode('utf-8'))
return SHAhash.hexdigest()
def _update_material_library_hash(self):
icon_dir = os.path.join(self.library_path, "icons")
os.makedirs(icon_dir, exist_ok=True)
icon_hash_path = os.path.join(icon_dir, "material_library_hash")
old_material_library_hash = None
if os.path.isfile(icon_hash_path):
with open(icon_hash_path, 'r', encoding='utf-8') as f:
old_material_library_hash = f.read()
current_material_library_hash = self._calculate_material_library_hash()
if current_material_library_hash != old_material_library_hash:
with open(icon_hash_path, 'w', encoding='utf-8') as f:
f.write(current_material_library_hash)
return True
return False
def _set_pixel(self, image, x, y, color):
data_offset = (x + int(y * image.size[0])) * 4
image.pixels[data_offset + 0] = color[0]
image.pixels[data_offset + 1] = color[1]
image.pixels[data_offset + 2] = color[2]
image.pixels[data_offset + 3] = color[3]
def _draw_icon_overlay(self, image, color):
width = image.size[0]
height = image.size[1]
tri_width = 16
size = tri_width
for j in range(0, tri_width):
for i in range(width - size, width):
self._set_pixel(image, i, j, color)
size -= 1
def _write_icon_to_file(self, material_preview, color, icon_path):
image = bpy.data.images.new(
"iconimg",
width=material_preview.icon_size[0],
height=material_preview.icon_size[1],
alpha=True
)
image.pixels = material_preview.icon_pixels_float
image.filepath_raw = icon_path
image.file_format = 'PNG'
self._draw_icon_overlay(image, color)
image.save()
bpy.data.images.remove(image)
def _generate_material_library_icons(self):
imported_material_names = []
for m in self.material_list:
with bpy.data.libraries.load(m.path) as (data_from, data_to):
data_to.materials = data_from.materials
for mdata in data_to.materials:
imported_material_names.append(mdata)
icon_path = os.path.join(self.library_path, "icons", m.name + ".png")
imported_icon_path = os.path.join(self.library_path, "icons", m.name + "_imported.png")
material = data_to.materials[0]
self._write_icon_to_file(material.preview, [1.0, 0.25, 0.02, 1.0], icon_path)
self._write_icon_to_file(material.preview, [0.0, 1.0, 0.23, 1.0], imported_icon_path)
for mname in imported_material_names:
material = bpy.data.materials.get(mname)
if material:
bpy.data.materials.remove(material)
def _initialize_preview_icons(self):
require_update = self._update_material_library_hash()
if require_update:
self._generate_material_library_icons()
self.material_icons.clear()
for m in self.material_list:
icon_path = os.path.join(self.library_path, "icons", m.name + ".png")
imported_icon_path = os.path.join(self.library_path, "icons", m.name + "_imported.png")
self.material_icons.load(m.name, icon_path, 'IMAGE')
self.material_icons.load(m.name + "_imported", imported_icon_path, 'IMAGE')
m.icon_id = self.material_icons[m.name].icon_id
m.imported_icon_id = self.material_icons[m.name + "_imported"].icon_id
def _get_material_data_from_name(self, name):
mdata = None
for m in self.material_list:
if m.name == name:
mdata = m
break
return mdata
def register():
bpy.utils.register_class(FLIPFluidMaterialLibraryMaterial)
bpy.utils.register_class(FLIPFluidMaterialLibrary)
def unregister():
bpy.utils.unregister_class(FLIPFluidMaterialLibraryMaterial)
bpy.utils.unregister_class(FLIPFluidMaterialLibrary)
@@ -0,0 +1,396 @@
# 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, json, os
from bpy.props import (
BoolProperty,
IntProperty,
StringProperty,
PointerProperty,
CollectionProperty
)
from ..presets import preset_library
from ..utils import preset_utils
from ..utils import version_compatibility_utils as vcu
class PresetStackProperty(bpy.types.PropertyGroup):
conv = vcu.convert_attribute_to_28
path = StringProperty(); exec(conv("path"))
value = StringProperty(); exec(conv("value"))
is_value_set = BoolProperty(default=False); exec(conv("is_value_set"))
def get_value(self):
if not self.is_value_set:
return None
return json.loads(self.value)
def set_value(self, value):
if value is None:
self.value = ""
self.is_value_set = False
return
self.value = json.dumps(value)
self.is_value_set = True
class PresetStackMaterialInfo(bpy.types.PropertyGroup):
conv = vcu.convert_attribute_to_28
preset_id = StringProperty(); exec(conv("preset_id"))
loaded_id = StringProperty(); exec(conv("loaded_id"))
class PresetStackElement(bpy.types.PropertyGroup):
conv = vcu.convert_attribute_to_28
is_enabled = BoolProperty(
name="Enabled",
description="Enable effects of preset in the stack",
default=True,
update=lambda self, context: self._update_is_enabled(context),
); exec(conv("is_enabled"))
is_applied = BoolProperty(default=False); exec(conv("is_applied"))
is_active = BoolProperty(default=True); exec(conv("is_active"))
identifier = StringProperty(); exec(conv("identifier"))
stack_uid = IntProperty(default=-1); exec(conv("stack_uid"))
saved_properties = CollectionProperty(type=PresetStackProperty); exec(conv("saved_properties"))
loaded_materials = CollectionProperty(type=PresetStackMaterialInfo); exec(conv("loaded_materials"))
def clear(self):
self.property_unset("is_applied")
self.property_unset("is_enabled")
self.property_unset("is_active")
self.property_unset("identifier")
self.property_unset("stack_uid")
self._clear_collection_property(self.saved_properties)
self._clear_collection_property(self.loaded_materials)
def save_properties(self, property_paths):
self._clear_collection_property(self.saved_properties)
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
for path in property_paths:
value = dprops.get_property_from_path(path)
if value is None:
continue
saved_property = self.saved_properties.add()
saved_property.path = path
saved_property.set_value(value)
def apply_saved_properties(self):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
for p in self.saved_properties:
dprops.set_property_from_path(p.path, p.get_value())
def apply_preset_properties(self):
dprops = bpy.context.scene.flip_fluid.get_domain_properties()
pinfo = preset_library.preset_identifier_to_info(self.identifier)
self._load_preset_materials(pinfo)
material_paths = preset_library.get_preset_material_paths()
for p in pinfo['properties']:
value = p['value']
minfo = None
if p['path'] in material_paths:
minfo = self._get_loaded_material_info(value)
if minfo is not None:
value = minfo.loaded_id
dprops.set_property_from_path(p['path'], value)
def unload_preset_materials(self):
preset_utils.unload_preset_materials(self.loaded_materials)
self._clear_collection_property(self.loaded_materials)
def copy(self, other):
self.is_applied = other.is_applied
self.identifier = other.identifier
for otherp in other.saved_properties:
thisp = self.saved_properties.add()
thisp.path = otherp.path
thisp.set_value(otherp.get_value())
for otherm in other.loaded_materials:
thism = self.loaded_materials.add()
thism.preset_id = otherm.preset_id
thism.loaded_id = otherm.loaded_id
def _get_loaded_material_info(self, preset_material_id):
for minfo in self.loaded_materials:
if minfo.preset_id == preset_material_id:
return minfo
return None
def _load_preset_materials(self, preset_info):
preset_utils.load_preset_materials(preset_info, self.loaded_materials)
def _update_is_enabled(self, context):
dprops = context.scene.flip_fluid.get_domain_properties()
stack = dprops.presets.preset_stack
for i,p in enumerate(stack.preset_stack):
if p.stack_uid == self.stack_uid:
if self.is_enabled:
stack.activate_preset(i)
else:
stack.deactivate_preset(i)
def _clear_collection_property(self, collection):
num_items = len(collection)
for i in range(num_items):
collection.remove(0)
class FlipFluidPresetStack(bpy.types.PropertyGroup):
conv = vcu.convert_attribute_to_28
is_enabled = BoolProperty(default=False); exec(conv("is_enabled"))
staged_preset = PointerProperty(type=PresetStackElement); exec(conv("staged_preset"))
is_preset_staged = BoolProperty(default=False); exec(conv("is_preset_staged"))
preset_stack = CollectionProperty(type=PresetStackElement); exec(conv("preset_stack"))
def enable(self):
self.is_enabled = True
self._apply_preset_stack()
def disable(self):
self.is_enabled = False
self._unapply_preset_stack()
def stage_preset(self, preset_id):
if self.is_preset_staged and self.staged_preset.identifier == preset_id:
return
if self.is_preset_staged:
self.unstage_preset()
if preset_id == 'PRESET_NONE':
return
self.staged_preset.identifier = preset_id
self._apply_preset(self.staged_preset)
self.is_preset_staged = True
def unstage_preset(self):
if not self.is_preset_staged:
return
self._unapply_preset(self.staged_preset)
self.staged_preset.clear()
self.is_preset_staged = False
def add_staged_preset_to_stack(self):
if not self.is_preset_staged:
return
uid = self._generate_stack_uid()
p = self.preset_stack.add()
p.copy(self.staged_preset)
p.stack_uid = uid
self.staged_preset.clear()
self.is_preset_staged = False
def remove_package_presets_from_stack(self, package_id):
preset_enums = preset_library.get_package_preset_enums(self, bpy.context, package_id)
preset_identifiers = []
for e in preset_enums:
if e[0] != 'PRESET_NONE':
preset_identifiers.append(e[0])
found_presets = False
for pe in self.preset_stack:
if pe.identifier in preset_identifiers:
found_presets = True
break
if not found_presets:
return
self._unapply_preset_stack()
for i in range(len(self.preset_stack) - 1, -1, -1):
if self.preset_stack[i].identifier in preset_identifiers:
self.preset_stack.remove(i)
self._apply_preset_stack()
def remove_preset_from_stack_by_identifier(self, preset_identifier):
for idx,se in enumerate(self.preset_stack):
if se.identifier == preset_identifier:
self.remove_preset_from_stack(idx)
return idx
return -1
def remove_preset_from_stack(self, stack_index):
if stack_index < 0 or stack_index >= len(self.preset_stack):
return
self._unapply_preset_stack()
self.preset_stack.remove(stack_index)
self._apply_preset_stack()
def apply_and_remove_preset_from_stack(self, stack_index):
if stack_index < 0 or stack_index >= len(self.preset_stack):
return
p = self.preset_stack[stack_index]
for minfo in p.loaded_materials:
m = bpy.data.materials.get(minfo.loaded_id)
if m is not None:
m.flip_fluid.skip_preset_unload = True
self.preset_stack.remove(stack_index)
def move_preset_up_in_stack(self, stack_index):
if stack_index <= 0 or stack_index >= len(self.preset_stack):
return
self._unapply_preset_stack()
self.preset_stack.move(stack_index, stack_index - 1)
self._apply_preset_stack()
def move_preset_down_in_stack(self, stack_index):
if stack_index < 0 or stack_index >= len(self.preset_stack) - 1:
return
self._unapply_preset_stack()
self.preset_stack.move(stack_index, stack_index + 1)
self._apply_preset_stack()
def insert_preset_into_stack(self, preset_identifier, stack_index):
#if stack_index < 0 or stack_index >= len(self.preset_stack) - 1:
# return
self._unapply_preset_stack()
self.stage_preset(preset_identifier)
self.add_staged_preset_to_stack()
self.preset_stack.move(len(self.preset_stack) - 1, stack_index)
self._apply_preset_stack()
def activate_preset(self, stack_index):
if stack_index < 0 or stack_index >= len(self.preset_stack):
return
if self.preset_stack[stack_index].is_active:
return
self._unapply_preset_stack()
self.preset_stack[stack_index].is_active = True
self._apply_preset_stack()
def deactivate_preset(self, stack_index):
if stack_index < 0 or stack_index >= len(self.preset_stack):
return
if not self.preset_stack[stack_index].is_active:
return
self._unapply_preset_stack()
self.preset_stack[stack_index].is_active = False
self._apply_preset_stack()
def is_preset_in_stack(self, preset_identifier):
for pe in self.preset_stack:
if pe.identifier == preset_identifier:
return True
return False
def validate_stack(self):
preset_info = preset_library.get_preset_info_list()
valid_identifiers = [info['identifier'] for info in preset_info]
for i in range(len(self.preset_stack) - 1, -1, -1):
pe = self.preset_stack[i]
if pe.identifier not in valid_identifiers:
errmsg = ("Preset Stack Warning: preset <" + pe.identifier +
"> not found on system, removing from stack")
print(errmsg)
self.preset_stack.remove(i)
def _apply_preset(self, stack_element):
pinfo = preset_library.preset_identifier_to_info(stack_element.identifier)
property_paths = []
for p in pinfo['properties']:
property_paths.append(p['path'])
stack_element.save_properties(property_paths)
stack_element.apply_preset_properties()
stack_element.is_applied = True
def _unapply_preset(self, stack_element):
stack_element.apply_saved_properties()
pid = stack_element.identifier
preset_count = 0
if self.is_preset_staged and self.staged_preset.identifier == pid:
if self.staged_preset.is_applied:
preset_count +=1
for p in self.preset_stack:
if p.identifier == pid and p.is_applied:
preset_count += 1
if preset_count == 1:
stack_element.unload_preset_materials()
stack_element.is_applied = False
def _apply_preset_stack(self):
for p in self.preset_stack:
if not p.is_applied and p.is_active:
self._apply_preset(p)
if self.is_preset_staged and not self.staged_preset.is_applied:
self._apply_preset(self.staged_preset)
def _unapply_preset_stack(self):
if self.is_preset_staged and self.staged_preset.is_applied:
self._unapply_preset(self.staged_preset)
for p in reversed(self.preset_stack):
if p.is_applied and p.is_active:
self._unapply_preset(p)
def _generate_stack_uid(self):
ids = [x.stack_uid for x in self.preset_stack]
for i in range(0, 1000000):
if not i in ids:
return i
return -1
def register():
bpy.utils.register_class(PresetStackProperty)
bpy.utils.register_class(PresetStackMaterialInfo)
bpy.utils.register_class(PresetStackElement)
bpy.utils.register_class(FlipFluidPresetStack)
def unregister():
bpy.utils.unregister_class(PresetStackProperty)
bpy.utils.unregister_class(PresetStackMaterialInfo)
bpy.utils.unregister_class(PresetStackElement)
bpy.utils.unregister_class(FlipFluidPresetStack)