Skip to content

Commit 4983132

Browse files
w1th0utnam3Fek04
andcommitted
Add "Splashsurf Studio" Blender add-on
Co-authored-by: Felix Kern <[email protected]>
1 parent e7c9525 commit 4983132

21 files changed

+1677
-0
lines changed

splashsurf_studio/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Splashsurf Studio
2+
Splashsurf Studio is a Blender add-on for on-the-fly surface reconstruction of fluid simulation data. It leverages the `pySplashsurf` Python library, which is based on the [`splashsurf`](https://splashsurf.physics-simulation.org/) tool and library.
3+
4+
The add-on interface can be found in the 3D Viewport toolbar (accessible via the `N` key) while in Object Mode.
5+
6+
Splashsurf Studio interprets the vertex positions of any Blender object as particle coordinates to perform surface reconstruction using `pySplashsurf`.
7+
A common workflow involves loading particle data from SPH (Smoothed Particle Hydrodynamics) simulations using the [Sequence Loader add-on](https://extensions.blender.org/add-ons/sequence-loader/).
8+
As the Sequence Loader updates the input file on each frame change, Splashsurf Studio automatically refreshes the surface reconstruction to reflect the new data.
9+
10+
### Note on Geometry Nodes
11+
12+
Currently, the add-on cannot use geometry generated by Geometry Nodes as input for surface reconstruction due to Blender's execution order.
13+
Only base geometry or geometry loaded via tools like the Sequence Loader add-on is supported.
14+
However, the reconstructed surface itself can be used as an input for Geometry Nodes.
15+
If you are interested in contributing to improve the flexibility of the add-on, please contact us via our [GitHub repository](https://github.com/InteractiveComputerGraphics/splashsurf).
16+
17+
If you want to use both, Geometry Nodes *and* Splashsurf, on the same input geometry (e.g. to render particles and surfaces in the same scene) it is recommended to create a separate object with a placeholder mesh (e.g. a cube) for Geometry Nodes.
18+
Inside the Geometry Nodes of this new object you can refer to the common input geometry object and ignore the placeholder geometry.
19+
20+
## Basic Workflow
21+
Assuming you have already imported your particle data (e.g., using the [Sequence Loader add-on](https://extensions.blender.org/add-ons/sequence-loader/)), you can set up the surface reconstruction as follows:
22+
23+
1. Select your particle data in the Outliner.
24+
![](img/select_input.jpg)
25+
1. Click the "+" button in the "Select Input" panel to use the currently selected object's vertices for reconstruction.
26+
![](img/register_input.jpg)
27+
2. Edit the reconstruction parameters in the "Viewport Settings" and "Render Settings" panels.
28+
Viewport settings are used when reconstructions are updated in the 3D Viewport, while render settings are applied to update reconstructions during rendering.
29+
Typically, you would use a low-resolution reconstruction in the viewport for better performance and high-resolution settings for renders.
30+
1. Activate automatic reconstruction in the 'Select Input' panel to start the process.
31+
![](img/addon_activate.jpg)
32+
33+
The add-on will now reconstruct a surface from the input vertices and automatically update it on frame changes (e.g., when the input data changes via the [Sequence Loader add-on](https://extensions.blender.org/add-ons/sequence-loader/)).
34+
35+
## Caching
36+
You can cache reconstructions for specific frames using the 'Cached Frames' panel.
37+
For frames added to the cache list, the surface reconstruction is performed once and then loaded from the cache when you switch to that frame.
38+
This allows you to quickly switch between specific frames to check camera and light settings, for example, without waiting for a new reconstruction.
39+
Changing reconstruction settings does not affect cached frames unless you manually regenerate them.
40+
41+
![Cached Frames Panel](img/cached_frames.png)
42+
43+
The "Cache Settings" property determines whether the viewport or render settings are used for the cache.
44+
45+
Add frames to the cache by clicking the "+" button.
46+
This opens a window where you can input a string of frames to be cached:
47+
48+
![Add Frames Window](img/add_frames.png)
49+
50+
This string adds frames 4, 10, 11, 12, 13, 14, 15, and 23 to the cache list.
51+
To remove frames, select them and click the "-" button, or use the "Clear Cache" operator to remove all frames.
52+
53+
Reconstructions for newly added frames are generated the next time the frame is changed.
54+
All cached reconstructions can be updated manually using the "Regenerate Cache" button.
55+
56+
## Other Operators
57+
- **Parse CLI String**: Extracts parameters from a `splashsurf` CLI command (e.g., `splashsurf reconstruct input_{}.vtk -r=0.025 ...`).
58+
- **Get CLI String**: Performs the reverse operation, creating a CLI option string from the selected parameters and printing it to the Blender console.
59+
- **Test Render Params**: Runs the surface reconstruction using the render settings without modifying the mesh. It outputs the reconstruction time, vertex and triangle counts, and approximate memory usage.
60+
61+
## Notes
62+
- When using the [Sequence Loader add-on](https://extensions.blender.org/add-ons/sequence-loader/), Splashsurf Studio requires the particle data object's global visibility to be enabled. However, it can be hidden in the viewport (by clicking the eye icon in the Outliner) without issues.
63+
- By default, a `COPY_TRANSFORMS` constraint is added to the surface reconstruction, matching its transform to the particle data. This prevents the surface from being moved independently. If this is not desired, simply remove the constraint.
64+
- If an error occurs during surface reconstruction, it will be displayed in a dedicated error panel:
65+
![Error log panel](img/error_log.png)
66+
- The add-on automatically enables `Render > Lock Interface` to prevent crashes during rendering.
67+
68+
## Acknowledgements
69+
70+
This add-on contains notable contributions from the following people:
71+
- [Felix Kern](https://github.com/Fek04) ([@Fek04](https://github.com/Fek04)) contributed the initial implementation of the Blender add-on and the `pySplashsurf` bindings
72+
- [Fabian Löschner](https://www.floeschner.de/) ([@w1th0utnam3](https://github.com/w1th0utnam3)) is the main author of `splashsurf` itself and current maintainer of the extension and Python bindings
73+

splashsurf_studio/__init__.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import bpy
2+
from .handlers import generate_mesh, toggle_rendering_off, toggle_rendering_on, ensure_frame_change_handlers
3+
from .properties import *
4+
from .operators import *
5+
from .panels import *
6+
from .updater import cached_meshes
7+
8+
classes = [
9+
SPSF_Frame_Properties,
10+
SPSF_Data_Obj_Properties,
11+
SPSF_Attribute_Properties,
12+
SPSF_Error_Message_Properties,
13+
SPSF_Reconstruction_Properties,
14+
SPSF_Scene_Properties,
15+
SPSF_OT_Activate,
16+
SPSF_OT_Deactivate,
17+
SPSF_OT_Copy_To_Render,
18+
SPSF_OT_Copy_To_Viewport,
19+
SPSF_OT_Get_CLI_String,
20+
SPSF_OT_Parse_CLI_Args,
21+
SPSF_OT_Register,
22+
SPSF_OT_Unregister,
23+
SPSF_OT_Test_Render_Params,
24+
SPSF_OT_Clear_Errors,
25+
SPSF_OT_Show_Error,
26+
SPSF_OT_Add_Frames,
27+
SPSF_OT_Remove_Frame,
28+
SPSF_OT_Clear_Cache,
29+
SPSF_OT_Regenerate_Cache,
30+
SPSF_OT_Jump_To_Frame,
31+
SPSF_UL_Attribute_List,
32+
SPSF_UL_Data_Object_List,
33+
SPSF_UL_Error_List,
34+
SPSF_Selection_Panel,
35+
SPSF_UL_Frame_List,
36+
SPSF_Caching_Panel,
37+
SPSF_Error_Log_Panel,
38+
SPSF_Viewport_Settings_Panel,
39+
SPSF_Viewport_Attributes_Panel,
40+
SPSF_Viewport_Params_Panel,
41+
SPSF_Render_Settings_Panel,
42+
SPSF_Render_Attributes_Panel,
43+
SPSF_Render_Params_Panel
44+
]
45+
46+
def register():
47+
for cls in classes:
48+
bpy.utils.register_class(cls)
49+
50+
bpy.types.Object.viewport_reconstruction_properties = bpy.props.PointerProperty(type=SPSF_Reconstruction_Properties)
51+
bpy.types.Object.render_reconstruction_properties = bpy.props.PointerProperty(type=SPSF_Reconstruction_Properties)
52+
bpy.types.Scene.splashsurf_studio = bpy.props.PointerProperty(type=SPSF_Scene_Properties)
53+
bpy.types.Mesh.cached = bpy.props.BoolProperty(default=False)
54+
55+
bpy.app.handlers.load_post.append(ensure_frame_change_handlers)
56+
57+
# Run once to ensure handlers are added when script is reloaded
58+
ensure_frame_change_handlers(None)
59+
60+
def unregister():
61+
for cls in classes:
62+
bpy.utils.unregister_class(cls)
63+
64+
bpy.app.handlers.load_post.remove(ensure_frame_change_handlers)
65+
66+
if generate_mesh in bpy.app.handlers.frame_change_post:
67+
bpy.app.handlers.frame_change_post.remove(generate_mesh)
68+
if toggle_rendering_on in bpy.app.handlers.render_init:
69+
bpy.app.handlers.render_init.remove(toggle_rendering_on)
70+
if toggle_rendering_off in bpy.app.handlers.render_complete:
71+
bpy.app.handlers.render_complete.remove(toggle_rendering_off)
72+
if toggle_rendering_off in bpy.app.handlers.render_cancel:
73+
bpy.app.handlers.render_cancel.remove(toggle_rendering_off)
74+
75+
del bpy.types.Object.viewport_reconstruction_properties
76+
del bpy.types.Object.render_reconstruction_properties
77+
del bpy.types.Scene.splashsurf_studio
78+
del bpy.types.Mesh.cached
79+
80+
# Unload cached meshes, only relevant if addon is reloaded
81+
clear_cache(cached_meshes)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
schema_version = "1.0.0"
2+
3+
# Example of manifest file for a Blender extension
4+
# Change the values according to your extension
5+
id = "splashsurf_studio"
6+
version = "1.0.0"
7+
name = "Splashsurf Studio"
8+
tagline = "Surface reconstruction for particle-based fluid simulations"
9+
maintainer = "Fabian Löschner"
10+
# Supported types: "add-on", "theme"
11+
type = "add-on"
12+
13+
# Optional link to documentation, support, source files, etc
14+
website = "https://splashsurf.physics-simulation.org/"
15+
16+
# Optional list defined by Blender and server, see:
17+
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
18+
tags = ["Add Mesh", "Mesh", "Object"]
19+
20+
blender_version_min = "4.5.0"
21+
# # Optional: Blender version that the extension does not support, earlier versions are supported.
22+
# # This can be omitted and defined later on the extensions platform if an issue is found.
23+
# blender_version_max = "5.1.0"
24+
25+
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
26+
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
27+
license = [
28+
"SPDX:GPL-3.0-or-later",
29+
]
30+
# Optional: required by some licenses.
31+
# copyright = [
32+
# "2002-2024 Developer Name",
33+
# "1998 Company Name",
34+
# ]
35+
36+
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
37+
platforms = ["windows-x64", "macos-arm64", "linux-x64", "windows-arm64", "macos-x64"]
38+
39+
# Optional: bundle 3rd party Python modules.
40+
# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
41+
wheels = [
42+
"./wheels/pysplashsurf-0.14.0.0-cp310-abi3-macosx_14_0_arm64.whl",
43+
"./wheels/pysplashsurf-0.14.0.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
44+
"./wheels/pysplashsurf-0.14.0.0-cp310-abi3-win_amd64.whl",
45+
"./wheels/pysplashsurf-0.14.0.0-cp310-abi3-win_arm64.whl",
46+
"./wheels/pysplashsurf-0.14.0.0-cp310-abi3-macosx_13_0_x86_64.whl"
47+
]
48+
49+
# Optional: add-ons can list which resources they will require:
50+
# * files (for access of any filesystem operations)
51+
# * network (for internet access)
52+
# * clipboard (to read and/or write the system clipboard)
53+
# * camera (to capture photos and videos)
54+
# * microphone (to capture audio)
55+
#
56+
# If using network, remember to also check `bpy.app.online_access`
57+
# https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
58+
#
59+
# For each permission it is important to also specify the reason why it is required.
60+
# Keep this a single short sentence without a period (.) at the end.
61+
# For longer explanations use the documentation or detail page.
62+
#
63+
# [permissions]
64+
# network = "Need to sync motion-capture data to server"
65+
# files = "Import/export FBX from/to disk"
66+
# clipboard = "Copy and paste bone transforms"
67+
68+
# Optional: build settings.
69+
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
70+
# [build]
71+
# paths_exclude_pattern = [
72+
# "__pycache__/",
73+
# "/.git/",
74+
# "/*.zip",
75+
# ]

splashsurf_studio/handlers.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import bpy
2+
from bpy.app.handlers import persistent
3+
4+
from .utils import get_selected_data_object
5+
from .updater import update_entries, update_reconstruction, cached_meshes
6+
7+
def generate_mesh(scene):
8+
if not scene.splashsurf_studio.active:
9+
return
10+
11+
update_entries(scene)
12+
13+
def toggle_rendering_on(scene):
14+
scene.splashsurf_studio.rendering = True
15+
16+
def toggle_rendering_off(scene):
17+
scene.splashsurf_studio.rendering = False
18+
19+
@persistent
20+
def ensure_frame_change_handlers(_):
21+
if generate_mesh not in bpy.app.handlers.frame_change_post:
22+
bpy.app.handlers.frame_change_post.append(generate_mesh)
23+
if toggle_rendering_on not in bpy.app.handlers.render_init:
24+
bpy.app.handlers.render_init.append(toggle_rendering_on)
25+
if toggle_rendering_off not in bpy.app.handlers.render_complete:
26+
bpy.app.handlers.render_complete.append(toggle_rendering_off)
27+
if toggle_rendering_off not in bpy.app.handlers.render_cancel:
28+
bpy.app.handlers.render_cancel.append(toggle_rendering_off)
29+
30+
def property_callback(self, context):
31+
if context.scene.splashsurf_studio.update_on_change and context.scene.splashsurf_studio.active:
32+
obj = get_selected_data_object(context)
33+
scene = context.scene
34+
frame = scene.frame_current
35+
36+
cache_frame = False
37+
for part in scene.splashsurf_studio.cached_frames:
38+
if frame == part.frame:
39+
cache_frame = True
40+
break
41+
42+
# Only regenerate if edited property would get used for current reconstruction
43+
if cache_frame:
44+
if scene.splashsurf_studio.cache_settings != self.type:
45+
return
46+
elif (scene.splashsurf_studio.use_render_for_viewport and self.type == 'VIEWPORT') or (not scene.splashsurf_studio.use_render_for_viewport and self.type == 'RENDER'):
47+
return
48+
49+
# Find the corresponding data object entry
50+
for entry in scene.splashsurf_studio.data_objs:
51+
if entry.data_pointer == obj:
52+
53+
# Use correct reconstruction properties
54+
use_render_props = scene.splashsurf_studio.use_render_for_viewport or scene.splashsurf_studio.rendering
55+
if cache_frame:
56+
match scene.splashsurf_studio.cache_settings:
57+
case 'VIEWPORT':
58+
use_render_props = False
59+
case 'RENDER':
60+
use_render_props = True
61+
62+
# Set cached flag to false so that update_reconstruction reuses the mesh data object
63+
if entry.surface_pointer is not None and entry.surface_pointer.data.cached:
64+
entry.surface_pointer.data.cached = False
65+
66+
update_reconstruction(scene, entry, use_render_props)
67+
68+
if cache_frame:
69+
if frame not in cached_meshes:
70+
cached_meshes[frame] = {}
71+
72+
entry.surface_pointer.data.cached = True
73+
cached_meshes[frame][entry.data_pointer] = entry.surface_pointer.data
74+
75+
break
76+
77+
# Callback to update the reconstruction once when the "Update on Change" property is turned on
78+
def update_callback(_, context):
79+
# Call property callback for both reconstruction properties, property_callback will abort for the not used type
80+
property_callback(get_selected_data_object(context).viewport_reconstruction_properties, context)
81+
property_callback(get_selected_data_object(context).render_reconstruction_properties, context)
82+
83+
def update_copy_transforms_constraints(self, context):
84+
if self.match_transforms:
85+
# Add constraint to all data objects
86+
for entry in context.scene.splashsurf_studio.data_objs:
87+
if entry.surface_pointer is not None:
88+
# Check if constraint already exists
89+
exists = False
90+
for constraint in entry.surface_pointer.constraints:
91+
if constraint.type == 'COPY_TRANSFORMS' and constraint.target == entry.data_pointer:
92+
exists = True
93+
break
94+
95+
if not exists:
96+
constraint = entry.surface_pointer.constraints.new(type='COPY_TRANSFORMS')
97+
constraint.target = entry.data_pointer
98+
else:
99+
# Remove constraints from all data objects
100+
for entry in context.scene.splashsurf_studio.data_objs:
101+
if entry.surface_pointer is not None:
102+
for constraint in entry.surface_pointer.constraints:
103+
if constraint.type == 'COPY_TRANSFORMS' and constraint.target == entry.data_pointer:
104+
entry.surface_pointer.constraints.remove(constraint)
105+
break
106+
8.45 KB
Loading
74.7 KB
Loading
23 KB
Loading
15.2 KB
Loading

splashsurf_studio/img/feature.jpg

574 KB
Loading
71.5 KB
Loading

0 commit comments

Comments
 (0)