feat(fluids): add Fluid3D simulation and rendering
This commit is contained in:
1 parent
dfc9f39b08
commit
92109ae643
161 files changed
+24955
-350
No files matched your search
@@ -57,7 +57,7 @@ eve_declare_module(NAME particles LAYER 5 SCRIPT Particles SLOT particles
|
||||
# Surface fluid simulation: particles constrained to mesh SDFs (flow down
|
||||
# surfaces, droplet coalescence) with screen-space surface reconstruction.
|
||||
eve_declare_module(NAME fluids LAYER 5 SCRIPT Fluids SLOT fluids
|
||||
DEPS gpgpu graphics physics physics_backend
|
||||
DEPS gpgpu graphics image model3d physics physics_backend
|
||||
GROUP 3d web)
|
||||
eve_declare_module(NAME procgen LAYER 5 SCRIPT Procgen SLOT procgen
|
||||
DEPS gpgpu graphics image map transaction
|
||||
@@ -140,7 +140,8 @@ eve_declare_module(NAME definitions_editor LAYER 7 DEPS definitions_editing edit
|
||||
eve_declare_module(NAME dialogue_editor LAYER 7 DEPS audio_editor dialogue_editing editor GROUP 3d web)
|
||||
eve_declare_module(NAME graphics_editor LAYER 7 SCRIPT GraphicsEditorModule SLOT graphicsEditor
|
||||
DEPS editor graphics graphics_editing GROUP 3d web)
|
||||
eve_declare_module(NAME fluids_editor LAYER 7 DEPS editor fluids fluids_editing graphics_editor GROUP 3d web)
|
||||
eve_declare_module(NAME fluids_editor LAYER 7 SCRIPT FluidsEditorModule SLOT fluidsEditor
|
||||
DEPS editor fluids fluids_editing graphics_editor GROUP 3d web)
|
||||
eve_declare_module(NAME hd2d_editor LAYER 7 DEPS editor hd2d_editing GROUP 3d)
|
||||
eve_declare_module(NAME housegen_editor LAYER 7 DEPS domain_gizmo_editor editor housegen_editing GROUP 3d)
|
||||
eve_declare_module(NAME input_editor LAYER 7 DEPS editor input_editing GROUP 2d 3d web)
|
||||
|
||||
+1467
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,27 @@
|
||||
// Opt-in check for the package ActorCOMTransform mass-center dependency.
|
||||
function verifyVolumeFluidActorCOM() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.spacing = 0.1;
|
||||
local first = clone checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
first.position = [0.0, 1.0, 0.0];
|
||||
first.actorGroup = 7;
|
||||
first.material.density = 1000.0;
|
||||
local second = clone first;
|
||||
second.material = clone first.material;
|
||||
second.position = [1.0, 1.0, 0.0];
|
||||
second.material.density = 500.0;
|
||||
local excluded = clone first;
|
||||
excluded.position = [1.5, 1.0, 0.0];
|
||||
excluded.actorGroup = 8;
|
||||
definition.particles = [first, second, excluded];
|
||||
local source = checked(fluids.newVolumeSimulator(definition));
|
||||
local properties = checked(source.actorMassProperties(7));
|
||||
if (properties.particleCount != 2 || fabs(properties.mass - 1.5) > 0.00001)
|
||||
throw "Actor mass summary is incorrect";
|
||||
if (fabs(properties.centerOfMass[0] - 1.0 / 3.0) > 0.00001 ||
|
||||
fabs(properties.centerOfMass[1] - 1.0) > 0.00001)
|
||||
throw "Actor center of mass is incorrect";
|
||||
if (source.actorMassProperties(9).ok)
|
||||
throw "Missing actor group was accepted";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,866 @@
|
||||
# Procedural torus fixture generated for EVEngine volume-fluid example.
|
||||
o emission_ring
|
||||
v 0.63000000 0.00000000 0.00000000
|
||||
v 0.60588457 0.09000000 0.00000000
|
||||
v 0.54000000 0.15588457 0.00000000
|
||||
v 0.45000000 0.18000000 0.00000000
|
||||
v 0.36000000 0.15588457 0.00000000
|
||||
v 0.29411543 0.09000000 0.00000000
|
||||
v 0.27000000 0.00000000 0.00000000
|
||||
v 0.29411543 -0.09000000 0.00000000
|
||||
v 0.36000000 -0.15588457 0.00000000
|
||||
v 0.45000000 -0.18000000 0.00000000
|
||||
v 0.54000000 -0.15588457 0.00000000
|
||||
v 0.60588457 -0.09000000 0.00000000
|
||||
v 0.60853327 0.00000000 0.16305600
|
||||
v 0.58523956 0.09000000 0.15681447
|
||||
v 0.52159995 0.15588457 0.13976228
|
||||
v 0.43466662 0.18000000 0.11646857
|
||||
v 0.34773330 0.15588457 0.09317486
|
||||
v 0.28409369 0.09000000 0.07612267
|
||||
v 0.26079997 0.00000000 0.06988114
|
||||
v 0.28409369 -0.09000000 0.07612267
|
||||
v 0.34773330 -0.15588457 0.09317486
|
||||
v 0.43466662 -0.18000000 0.11646857
|
||||
v 0.52159995 -0.15588457 0.13976228
|
||||
v 0.58523956 -0.09000000 0.15681447
|
||||
v 0.54559600 0.00000000 0.31500000
|
||||
v 0.52471143 0.09000000 0.30294229
|
||||
v 0.46765372 0.15588457 0.27000000
|
||||
v 0.38971143 0.18000000 0.22500000
|
||||
v 0.31176915 0.15588457 0.18000000
|
||||
v 0.25471143 0.09000000 0.14705771
|
||||
v 0.23382686 0.00000000 0.13500000
|
||||
v 0.25471143 -0.09000000 0.14705771
|
||||
v 0.31176915 -0.15588457 0.18000000
|
||||
v 0.38971143 -0.18000000 0.22500000
|
||||
v 0.46765372 -0.15588457 0.27000000
|
||||
v 0.52471143 -0.09000000 0.30294229
|
||||
v 0.44547727 0.00000000 0.44547727
|
||||
v 0.42842509 0.09000000 0.42842509
|
||||
v 0.38183766 0.15588457 0.38183766
|
||||
v 0.31819805 0.18000000 0.31819805
|
||||
v 0.25455844 0.15588457 0.25455844
|
||||
v 0.20797101 0.09000000 0.20797101
|
||||
v 0.19091883 0.00000000 0.19091883
|
||||
v 0.20797101 -0.09000000 0.20797101
|
||||
v 0.25455844 -0.15588457 0.25455844
|
||||
v 0.31819805 -0.18000000 0.31819805
|
||||
v 0.38183766 -0.15588457 0.38183766
|
||||
v 0.42842509 -0.09000000 0.42842509
|
||||
v 0.31500000 0.00000000 0.54559600
|
||||
v 0.30294229 0.09000000 0.52471143
|
||||
v 0.27000000 0.15588457 0.46765372
|
||||
v 0.22500000 0.18000000 0.38971143
|
||||
v 0.18000000 0.15588457 0.31176915
|
||||
v 0.14705771 0.09000000 0.25471143
|
||||
v 0.13500000 0.00000000 0.23382686
|
||||
v 0.14705771 -0.09000000 0.25471143
|
||||
v 0.18000000 -0.15588457 0.31176915
|
||||
v 0.22500000 -0.18000000 0.38971143
|
||||
v 0.27000000 -0.15588457 0.46765372
|
||||
v 0.30294229 -0.09000000 0.52471143
|
||||
v 0.16305600 0.00000000 0.60853327
|
||||
v 0.15681447 0.09000000 0.58523956
|
||||
v 0.13976228 0.15588457 0.52159995
|
||||
v 0.11646857 0.18000000 0.43466662
|
||||
v 0.09317486 0.15588457 0.34773330
|
||||
v 0.07612267 0.09000000 0.28409369
|
||||
v 0.06988114 0.00000000 0.26079997
|
||||
v 0.07612267 -0.09000000 0.28409369
|
||||
v 0.09317486 -0.15588457 0.34773330
|
||||
v 0.11646857 -0.18000000 0.43466662
|
||||
v 0.13976228 -0.15588457 0.52159995
|
||||
v 0.15681447 -0.09000000 0.58523956
|
||||
v 0.00000000 0.00000000 0.63000000
|
||||
v 0.00000000 0.09000000 0.60588457
|
||||
v 0.00000000 0.15588457 0.54000000
|
||||
v 0.00000000 0.18000000 0.45000000
|
||||
v 0.00000000 0.15588457 0.36000000
|
||||
v 0.00000000 0.09000000 0.29411543
|
||||
v 0.00000000 0.00000000 0.27000000
|
||||
v 0.00000000 -0.09000000 0.29411543
|
||||
v 0.00000000 -0.15588457 0.36000000
|
||||
v 0.00000000 -0.18000000 0.45000000
|
||||
v 0.00000000 -0.15588457 0.54000000
|
||||
v 0.00000000 -0.09000000 0.60588457
|
||||
v -0.16305600 0.00000000 0.60853327
|
||||
v -0.15681447 0.09000000 0.58523956
|
||||
v -0.13976228 0.15588457 0.52159995
|
||||
v -0.11646857 0.18000000 0.43466662
|
||||
v -0.09317486 0.15588457 0.34773330
|
||||
v -0.07612267 0.09000000 0.28409369
|
||||
v -0.06988114 0.00000000 0.26079997
|
||||
v -0.07612267 -0.09000000 0.28409369
|
||||
v -0.09317486 -0.15588457 0.34773330
|
||||
v -0.11646857 -0.18000000 0.43466662
|
||||
v -0.13976228 -0.15588457 0.52159995
|
||||
v -0.15681447 -0.09000000 0.58523956
|
||||
v -0.31500000 0.00000000 0.54559600
|
||||
v -0.30294229 0.09000000 0.52471143
|
||||
v -0.27000000 0.15588457 0.46765372
|
||||
v -0.22500000 0.18000000 0.38971143
|
||||
v -0.18000000 0.15588457 0.31176915
|
||||
v -0.14705771 0.09000000 0.25471143
|
||||
v -0.13500000 0.00000000 0.23382686
|
||||
v -0.14705771 -0.09000000 0.25471143
|
||||
v -0.18000000 -0.15588457 0.31176915
|
||||
v -0.22500000 -0.18000000 0.38971143
|
||||
v -0.27000000 -0.15588457 0.46765372
|
||||
v -0.30294229 -0.09000000 0.52471143
|
||||
v -0.44547727 0.00000000 0.44547727
|
||||
v -0.42842509 0.09000000 0.42842509
|
||||
v -0.38183766 0.15588457 0.38183766
|
||||
v -0.31819805 0.18000000 0.31819805
|
||||
v -0.25455844 0.15588457 0.25455844
|
||||
v -0.20797101 0.09000000 0.20797101
|
||||
v -0.19091883 0.00000000 0.19091883
|
||||
v -0.20797101 -0.09000000 0.20797101
|
||||
v -0.25455844 -0.15588457 0.25455844
|
||||
v -0.31819805 -0.18000000 0.31819805
|
||||
v -0.38183766 -0.15588457 0.38183766
|
||||
v -0.42842509 -0.09000000 0.42842509
|
||||
v -0.54559600 0.00000000 0.31500000
|
||||
v -0.52471143 0.09000000 0.30294229
|
||||
v -0.46765372 0.15588457 0.27000000
|
||||
v -0.38971143 0.18000000 0.22500000
|
||||
v -0.31176915 0.15588457 0.18000000
|
||||
v -0.25471143 0.09000000 0.14705771
|
||||
v -0.23382686 0.00000000 0.13500000
|
||||
v -0.25471143 -0.09000000 0.14705771
|
||||
v -0.31176915 -0.15588457 0.18000000
|
||||
v -0.38971143 -0.18000000 0.22500000
|
||||
v -0.46765372 -0.15588457 0.27000000
|
||||
v -0.52471143 -0.09000000 0.30294229
|
||||
v -0.60853327 0.00000000 0.16305600
|
||||
v -0.58523956 0.09000000 0.15681447
|
||||
v -0.52159995 0.15588457 0.13976228
|
||||
v -0.43466662 0.18000000 0.11646857
|
||||
v -0.34773330 0.15588457 0.09317486
|
||||
v -0.28409369 0.09000000 0.07612267
|
||||
v -0.26079997 0.00000000 0.06988114
|
||||
v -0.28409369 -0.09000000 0.07612267
|
||||
v -0.34773330 -0.15588457 0.09317486
|
||||
v -0.43466662 -0.18000000 0.11646857
|
||||
v -0.52159995 -0.15588457 0.13976228
|
||||
v -0.58523956 -0.09000000 0.15681447
|
||||
v -0.63000000 0.00000000 0.00000000
|
||||
v -0.60588457 0.09000000 0.00000000
|
||||
v -0.54000000 0.15588457 0.00000000
|
||||
v -0.45000000 0.18000000 0.00000000
|
||||
v -0.36000000 0.15588457 0.00000000
|
||||
v -0.29411543 0.09000000 0.00000000
|
||||
v -0.27000000 0.00000000 0.00000000
|
||||
v -0.29411543 -0.09000000 0.00000000
|
||||
v -0.36000000 -0.15588457 0.00000000
|
||||
v -0.45000000 -0.18000000 0.00000000
|
||||
v -0.54000000 -0.15588457 0.00000000
|
||||
v -0.60588457 -0.09000000 0.00000000
|
||||
v -0.60853327 0.00000000 -0.16305600
|
||||
v -0.58523956 0.09000000 -0.15681447
|
||||
v -0.52159995 0.15588457 -0.13976228
|
||||
v -0.43466662 0.18000000 -0.11646857
|
||||
v -0.34773330 0.15588457 -0.09317486
|
||||
v -0.28409369 0.09000000 -0.07612267
|
||||
v -0.26079997 0.00000000 -0.06988114
|
||||
v -0.28409369 -0.09000000 -0.07612267
|
||||
v -0.34773330 -0.15588457 -0.09317486
|
||||
v -0.43466662 -0.18000000 -0.11646857
|
||||
v -0.52159995 -0.15588457 -0.13976228
|
||||
v -0.58523956 -0.09000000 -0.15681447
|
||||
v -0.54559600 0.00000000 -0.31500000
|
||||
v -0.52471143 0.09000000 -0.30294229
|
||||
v -0.46765372 0.15588457 -0.27000000
|
||||
v -0.38971143 0.18000000 -0.22500000
|
||||
v -0.31176915 0.15588457 -0.18000000
|
||||
v -0.25471143 0.09000000 -0.14705771
|
||||
v -0.23382686 0.00000000 -0.13500000
|
||||
v -0.25471143 -0.09000000 -0.14705771
|
||||
v -0.31176915 -0.15588457 -0.18000000
|
||||
v -0.38971143 -0.18000000 -0.22500000
|
||||
v -0.46765372 -0.15588457 -0.27000000
|
||||
v -0.52471143 -0.09000000 -0.30294229
|
||||
v -0.44547727 0.00000000 -0.44547727
|
||||
v -0.42842509 0.09000000 -0.42842509
|
||||
v -0.38183766 0.15588457 -0.38183766
|
||||
v -0.31819805 0.18000000 -0.31819805
|
||||
v -0.25455844 0.15588457 -0.25455844
|
||||
v -0.20797101 0.09000000 -0.20797101
|
||||
v -0.19091883 0.00000000 -0.19091883
|
||||
v -0.20797101 -0.09000000 -0.20797101
|
||||
v -0.25455844 -0.15588457 -0.25455844
|
||||
v -0.31819805 -0.18000000 -0.31819805
|
||||
v -0.38183766 -0.15588457 -0.38183766
|
||||
v -0.42842509 -0.09000000 -0.42842509
|
||||
v -0.31500000 0.00000000 -0.54559600
|
||||
v -0.30294229 0.09000000 -0.52471143
|
||||
v -0.27000000 0.15588457 -0.46765372
|
||||
v -0.22500000 0.18000000 -0.38971143
|
||||
v -0.18000000 0.15588457 -0.31176915
|
||||
v -0.14705771 0.09000000 -0.25471143
|
||||
v -0.13500000 0.00000000 -0.23382686
|
||||
v -0.14705771 -0.09000000 -0.25471143
|
||||
v -0.18000000 -0.15588457 -0.31176915
|
||||
v -0.22500000 -0.18000000 -0.38971143
|
||||
v -0.27000000 -0.15588457 -0.46765372
|
||||
v -0.30294229 -0.09000000 -0.52471143
|
||||
v -0.16305600 0.00000000 -0.60853327
|
||||
v -0.15681447 0.09000000 -0.58523956
|
||||
v -0.13976228 0.15588457 -0.52159995
|
||||
v -0.11646857 0.18000000 -0.43466662
|
||||
v -0.09317486 0.15588457 -0.34773330
|
||||
v -0.07612267 0.09000000 -0.28409369
|
||||
v -0.06988114 0.00000000 -0.26079997
|
||||
v -0.07612267 -0.09000000 -0.28409369
|
||||
v -0.09317486 -0.15588457 -0.34773330
|
||||
v -0.11646857 -0.18000000 -0.43466662
|
||||
v -0.13976228 -0.15588457 -0.52159995
|
||||
v -0.15681447 -0.09000000 -0.58523956
|
||||
v -0.00000000 0.00000000 -0.63000000
|
||||
v -0.00000000 0.09000000 -0.60588457
|
||||
v -0.00000000 0.15588457 -0.54000000
|
||||
v -0.00000000 0.18000000 -0.45000000
|
||||
v -0.00000000 0.15588457 -0.36000000
|
||||
v -0.00000000 0.09000000 -0.29411543
|
||||
v -0.00000000 0.00000000 -0.27000000
|
||||
v -0.00000000 -0.09000000 -0.29411543
|
||||
v -0.00000000 -0.15588457 -0.36000000
|
||||
v -0.00000000 -0.18000000 -0.45000000
|
||||
v -0.00000000 -0.15588457 -0.54000000
|
||||
v -0.00000000 -0.09000000 -0.60588457
|
||||
v 0.16305600 0.00000000 -0.60853327
|
||||
v 0.15681447 0.09000000 -0.58523956
|
||||
v 0.13976228 0.15588457 -0.52159995
|
||||
v 0.11646857 0.18000000 -0.43466662
|
||||
v 0.09317486 0.15588457 -0.34773330
|
||||
v 0.07612267 0.09000000 -0.28409369
|
||||
v 0.06988114 0.00000000 -0.26079997
|
||||
v 0.07612267 -0.09000000 -0.28409369
|
||||
v 0.09317486 -0.15588457 -0.34773330
|
||||
v 0.11646857 -0.18000000 -0.43466662
|
||||
v 0.13976228 -0.15588457 -0.52159995
|
||||
v 0.15681447 -0.09000000 -0.58523956
|
||||
v 0.31500000 0.00000000 -0.54559600
|
||||
v 0.30294229 0.09000000 -0.52471143
|
||||
v 0.27000000 0.15588457 -0.46765372
|
||||
v 0.22500000 0.18000000 -0.38971143
|
||||
v 0.18000000 0.15588457 -0.31176915
|
||||
v 0.14705771 0.09000000 -0.25471143
|
||||
v 0.13500000 0.00000000 -0.23382686
|
||||
v 0.14705771 -0.09000000 -0.25471143
|
||||
v 0.18000000 -0.15588457 -0.31176915
|
||||
v 0.22500000 -0.18000000 -0.38971143
|
||||
v 0.27000000 -0.15588457 -0.46765372
|
||||
v 0.30294229 -0.09000000 -0.52471143
|
||||
v 0.44547727 0.00000000 -0.44547727
|
||||
v 0.42842509 0.09000000 -0.42842509
|
||||
v 0.38183766 0.15588457 -0.38183766
|
||||
v 0.31819805 0.18000000 -0.31819805
|
||||
v 0.25455844 0.15588457 -0.25455844
|
||||
v 0.20797101 0.09000000 -0.20797101
|
||||
v 0.19091883 0.00000000 -0.19091883
|
||||
v 0.20797101 -0.09000000 -0.20797101
|
||||
v 0.25455844 -0.15588457 -0.25455844
|
||||
v 0.31819805 -0.18000000 -0.31819805
|
||||
v 0.38183766 -0.15588457 -0.38183766
|
||||
v 0.42842509 -0.09000000 -0.42842509
|
||||
v 0.54559600 0.00000000 -0.31500000
|
||||
v 0.52471143 0.09000000 -0.30294229
|
||||
v 0.46765372 0.15588457 -0.27000000
|
||||
v 0.38971143 0.18000000 -0.22500000
|
||||
v 0.31176915 0.15588457 -0.18000000
|
||||
v 0.25471143 0.09000000 -0.14705771
|
||||
v 0.23382686 0.00000000 -0.13500000
|
||||
v 0.25471143 -0.09000000 -0.14705771
|
||||
v 0.31176915 -0.15588457 -0.18000000
|
||||
v 0.38971143 -0.18000000 -0.22500000
|
||||
v 0.46765372 -0.15588457 -0.27000000
|
||||
v 0.52471143 -0.09000000 -0.30294229
|
||||
v 0.60853327 0.00000000 -0.16305600
|
||||
v 0.58523956 0.09000000 -0.15681447
|
||||
v 0.52159995 0.15588457 -0.13976228
|
||||
v 0.43466662 0.18000000 -0.11646857
|
||||
v 0.34773330 0.15588457 -0.09317486
|
||||
v 0.28409369 0.09000000 -0.07612267
|
||||
v 0.26079997 0.00000000 -0.06988114
|
||||
v 0.28409369 -0.09000000 -0.07612267
|
||||
v 0.34773330 -0.15588457 -0.09317486
|
||||
v 0.43466662 -0.18000000 -0.11646857
|
||||
v 0.52159995 -0.15588457 -0.13976228
|
||||
v 0.58523956 -0.09000000 -0.15681447
|
||||
f 1 13 14
|
||||
f 1 14 2
|
||||
f 2 14 15
|
||||
f 2 15 3
|
||||
f 3 15 16
|
||||
f 3 16 4
|
||||
f 4 16 17
|
||||
f 4 17 5
|
||||
f 5 17 18
|
||||
f 5 18 6
|
||||
f 6 18 19
|
||||
f 6 19 7
|
||||
f 7 19 20
|
||||
f 7 20 8
|
||||
f 8 20 21
|
||||
f 8 21 9
|
||||
f 9 21 22
|
||||
f 9 22 10
|
||||
f 10 22 23
|
||||
f 10 23 11
|
||||
f 11 23 24
|
||||
f 11 24 12
|
||||
f 12 24 13
|
||||
f 12 13 1
|
||||
f 13 25 26
|
||||
f 13 26 14
|
||||
f 14 26 27
|
||||
f 14 27 15
|
||||
f 15 27 28
|
||||
f 15 28 16
|
||||
f 16 28 29
|
||||
f 16 29 17
|
||||
f 17 29 30
|
||||
f 17 30 18
|
||||
f 18 30 31
|
||||
f 18 31 19
|
||||
f 19 31 32
|
||||
f 19 32 20
|
||||
f 20 32 33
|
||||
f 20 33 21
|
||||
f 21 33 34
|
||||
f 21 34 22
|
||||
f 22 34 35
|
||||
f 22 35 23
|
||||
f 23 35 36
|
||||
f 23 36 24
|
||||
f 24 36 25
|
||||
f 24 25 13
|
||||
f 25 37 38
|
||||
f 25 38 26
|
||||
f 26 38 39
|
||||
f 26 39 27
|
||||
f 27 39 40
|
||||
f 27 40 28
|
||||
f 28 40 41
|
||||
f 28 41 29
|
||||
f 29 41 42
|
||||
f 29 42 30
|
||||
f 30 42 43
|
||||
f 30 43 31
|
||||
f 31 43 44
|
||||
f 31 44 32
|
||||
f 32 44 45
|
||||
f 32 45 33
|
||||
f 33 45 46
|
||||
f 33 46 34
|
||||
f 34 46 47
|
||||
f 34 47 35
|
||||
f 35 47 48
|
||||
f 35 48 36
|
||||
f 36 48 37
|
||||
f 36 37 25
|
||||
f 37 49 50
|
||||
f 37 50 38
|
||||
f 38 50 51
|
||||
f 38 51 39
|
||||
f 39 51 52
|
||||
f 39 52 40
|
||||
f 40 52 53
|
||||
f 40 53 41
|
||||
f 41 53 54
|
||||
f 41 54 42
|
||||
f 42 54 55
|
||||
f 42 55 43
|
||||
f 43 55 56
|
||||
f 43 56 44
|
||||
f 44 56 57
|
||||
f 44 57 45
|
||||
f 45 57 58
|
||||
f 45 58 46
|
||||
f 46 58 59
|
||||
f 46 59 47
|
||||
f 47 59 60
|
||||
f 47 60 48
|
||||
f 48 60 49
|
||||
f 48 49 37
|
||||
f 49 61 62
|
||||
f 49 62 50
|
||||
f 50 62 63
|
||||
f 50 63 51
|
||||
f 51 63 64
|
||||
f 51 64 52
|
||||
f 52 64 65
|
||||
f 52 65 53
|
||||
f 53 65 66
|
||||
f 53 66 54
|
||||
f 54 66 67
|
||||
f 54 67 55
|
||||
f 55 67 68
|
||||
f 55 68 56
|
||||
f 56 68 69
|
||||
f 56 69 57
|
||||
f 57 69 70
|
||||
f 57 70 58
|
||||
f 58 70 71
|
||||
f 58 71 59
|
||||
f 59 71 72
|
||||
f 59 72 60
|
||||
f 60 72 61
|
||||
f 60 61 49
|
||||
f 61 73 74
|
||||
f 61 74 62
|
||||
f 62 74 75
|
||||
f 62 75 63
|
||||
f 63 75 76
|
||||
f 63 76 64
|
||||
f 64 76 77
|
||||
f 64 77 65
|
||||
f 65 77 78
|
||||
f 65 78 66
|
||||
f 66 78 79
|
||||
f 66 79 67
|
||||
f 67 79 80
|
||||
f 67 80 68
|
||||
f 68 80 81
|
||||
f 68 81 69
|
||||
f 69 81 82
|
||||
f 69 82 70
|
||||
f 70 82 83
|
||||
f 70 83 71
|
||||
f 71 83 84
|
||||
f 71 84 72
|
||||
f 72 84 73
|
||||
f 72 73 61
|
||||
f 73 85 86
|
||||
f 73 86 74
|
||||
f 74 86 87
|
||||
f 74 87 75
|
||||
f 75 87 88
|
||||
f 75 88 76
|
||||
f 76 88 89
|
||||
f 76 89 77
|
||||
f 77 89 90
|
||||
f 77 90 78
|
||||
f 78 90 91
|
||||
f 78 91 79
|
||||
f 79 91 92
|
||||
f 79 92 80
|
||||
f 80 92 93
|
||||
f 80 93 81
|
||||
f 81 93 94
|
||||
f 81 94 82
|
||||
f 82 94 95
|
||||
f 82 95 83
|
||||
f 83 95 96
|
||||
f 83 96 84
|
||||
f 84 96 85
|
||||
f 84 85 73
|
||||
f 85 97 98
|
||||
f 85 98 86
|
||||
f 86 98 99
|
||||
f 86 99 87
|
||||
f 87 99 100
|
||||
f 87 100 88
|
||||
f 88 100 101
|
||||
f 88 101 89
|
||||
f 89 101 102
|
||||
f 89 102 90
|
||||
f 90 102 103
|
||||
f 90 103 91
|
||||
f 91 103 104
|
||||
f 91 104 92
|
||||
f 92 104 105
|
||||
f 92 105 93
|
||||
f 93 105 106
|
||||
f 93 106 94
|
||||
f 94 106 107
|
||||
f 94 107 95
|
||||
f 95 107 108
|
||||
f 95 108 96
|
||||
f 96 108 97
|
||||
f 96 97 85
|
||||
f 97 109 110
|
||||
f 97 110 98
|
||||
f 98 110 111
|
||||
f 98 111 99
|
||||
f 99 111 112
|
||||
f 99 112 100
|
||||
f 100 112 113
|
||||
f 100 113 101
|
||||
f 101 113 114
|
||||
f 101 114 102
|
||||
f 102 114 115
|
||||
f 102 115 103
|
||||
f 103 115 116
|
||||
f 103 116 104
|
||||
f 104 116 117
|
||||
f 104 117 105
|
||||
f 105 117 118
|
||||
f 105 118 106
|
||||
f 106 118 119
|
||||
f 106 119 107
|
||||
f 107 119 120
|
||||
f 107 120 108
|
||||
f 108 120 109
|
||||
f 108 109 97
|
||||
f 109 121 122
|
||||
f 109 122 110
|
||||
f 110 122 123
|
||||
f 110 123 111
|
||||
f 111 123 124
|
||||
f 111 124 112
|
||||
f 112 124 125
|
||||
f 112 125 113
|
||||
f 113 125 126
|
||||
f 113 126 114
|
||||
f 114 126 127
|
||||
f 114 127 115
|
||||
f 115 127 128
|
||||
f 115 128 116
|
||||
f 116 128 129
|
||||
f 116 129 117
|
||||
f 117 129 130
|
||||
f 117 130 118
|
||||
f 118 130 131
|
||||
f 118 131 119
|
||||
f 119 131 132
|
||||
f 119 132 120
|
||||
f 120 132 121
|
||||
f 120 121 109
|
||||
f 121 133 134
|
||||
f 121 134 122
|
||||
f 122 134 135
|
||||
f 122 135 123
|
||||
f 123 135 136
|
||||
f 123 136 124
|
||||
f 124 136 137
|
||||
f 124 137 125
|
||||
f 125 137 138
|
||||
f 125 138 126
|
||||
f 126 138 139
|
||||
f 126 139 127
|
||||
f 127 139 140
|
||||
f 127 140 128
|
||||
f 128 140 141
|
||||
f 128 141 129
|
||||
f 129 141 142
|
||||
f 129 142 130
|
||||
f 130 142 143
|
||||
f 130 143 131
|
||||
f 131 143 144
|
||||
f 131 144 132
|
||||
f 132 144 133
|
||||
f 132 133 121
|
||||
f 133 145 146
|
||||
f 133 146 134
|
||||
f 134 146 147
|
||||
f 134 147 135
|
||||
f 135 147 148
|
||||
f 135 148 136
|
||||
f 136 148 149
|
||||
f 136 149 137
|
||||
f 137 149 150
|
||||
f 137 150 138
|
||||
f 138 150 151
|
||||
f 138 151 139
|
||||
f 139 151 152
|
||||
f 139 152 140
|
||||
f 140 152 153
|
||||
f 140 153 141
|
||||
f 141 153 154
|
||||
f 141 154 142
|
||||
f 142 154 155
|
||||
f 142 155 143
|
||||
f 143 155 156
|
||||
f 143 156 144
|
||||
f 144 156 145
|
||||
f 144 145 133
|
||||
f 145 157 158
|
||||
f 145 158 146
|
||||
f 146 158 159
|
||||
f 146 159 147
|
||||
f 147 159 160
|
||||
f 147 160 148
|
||||
f 148 160 161
|
||||
f 148 161 149
|
||||
f 149 161 162
|
||||
f 149 162 150
|
||||
f 150 162 163
|
||||
f 150 163 151
|
||||
f 151 163 164
|
||||
f 151 164 152
|
||||
f 152 164 165
|
||||
f 152 165 153
|
||||
f 153 165 166
|
||||
f 153 166 154
|
||||
f 154 166 167
|
||||
f 154 167 155
|
||||
f 155 167 168
|
||||
f 155 168 156
|
||||
f 156 168 157
|
||||
f 156 157 145
|
||||
f 157 169 170
|
||||
f 157 170 158
|
||||
f 158 170 171
|
||||
f 158 171 159
|
||||
f 159 171 172
|
||||
f 159 172 160
|
||||
f 160 172 173
|
||||
f 160 173 161
|
||||
f 161 173 174
|
||||
f 161 174 162
|
||||
f 162 174 175
|
||||
f 162 175 163
|
||||
f 163 175 176
|
||||
f 163 176 164
|
||||
f 164 176 177
|
||||
f 164 177 165
|
||||
f 165 177 178
|
||||
f 165 178 166
|
||||
f 166 178 179
|
||||
f 166 179 167
|
||||
f 167 179 180
|
||||
f 167 180 168
|
||||
f 168 180 169
|
||||
f 168 169 157
|
||||
f 169 181 182
|
||||
f 169 182 170
|
||||
f 170 182 183
|
||||
f 170 183 171
|
||||
f 171 183 184
|
||||
f 171 184 172
|
||||
f 172 184 185
|
||||
f 172 185 173
|
||||
f 173 185 186
|
||||
f 173 186 174
|
||||
f 174 186 187
|
||||
f 174 187 175
|
||||
f 175 187 188
|
||||
f 175 188 176
|
||||
f 176 188 189
|
||||
f 176 189 177
|
||||
f 177 189 190
|
||||
f 177 190 178
|
||||
f 178 190 191
|
||||
f 178 191 179
|
||||
f 179 191 192
|
||||
f 179 192 180
|
||||
f 180 192 181
|
||||
f 180 181 169
|
||||
f 181 193 194
|
||||
f 181 194 182
|
||||
f 182 194 195
|
||||
f 182 195 183
|
||||
f 183 195 196
|
||||
f 183 196 184
|
||||
f 184 196 197
|
||||
f 184 197 185
|
||||
f 185 197 198
|
||||
f 185 198 186
|
||||
f 186 198 199
|
||||
f 186 199 187
|
||||
f 187 199 200
|
||||
f 187 200 188
|
||||
f 188 200 201
|
||||
f 188 201 189
|
||||
f 189 201 202
|
||||
f 189 202 190
|
||||
f 190 202 203
|
||||
f 190 203 191
|
||||
f 191 203 204
|
||||
f 191 204 192
|
||||
f 192 204 193
|
||||
f 192 193 181
|
||||
f 193 205 206
|
||||
f 193 206 194
|
||||
f 194 206 207
|
||||
f 194 207 195
|
||||
f 195 207 208
|
||||
f 195 208 196
|
||||
f 196 208 209
|
||||
f 196 209 197
|
||||
f 197 209 210
|
||||
f 197 210 198
|
||||
f 198 210 211
|
||||
f 198 211 199
|
||||
f 199 211 212
|
||||
f 199 212 200
|
||||
f 200 212 213
|
||||
f 200 213 201
|
||||
f 201 213 214
|
||||
f 201 214 202
|
||||
f 202 214 215
|
||||
f 202 215 203
|
||||
f 203 215 216
|
||||
f 203 216 204
|
||||
f 204 216 205
|
||||
f 204 205 193
|
||||
f 205 217 218
|
||||
f 205 218 206
|
||||
f 206 218 219
|
||||
f 206 219 207
|
||||
f 207 219 220
|
||||
f 207 220 208
|
||||
f 208 220 221
|
||||
f 208 221 209
|
||||
f 209 221 222
|
||||
f 209 222 210
|
||||
f 210 222 223
|
||||
f 210 223 211
|
||||
f 211 223 224
|
||||
f 211 224 212
|
||||
f 212 224 225
|
||||
f 212 225 213
|
||||
f 213 225 226
|
||||
f 213 226 214
|
||||
f 214 226 227
|
||||
f 214 227 215
|
||||
f 215 227 228
|
||||
f 215 228 216
|
||||
f 216 228 217
|
||||
f 216 217 205
|
||||
f 217 229 230
|
||||
f 217 230 218
|
||||
f 218 230 231
|
||||
f 218 231 219
|
||||
f 219 231 232
|
||||
f 219 232 220
|
||||
f 220 232 233
|
||||
f 220 233 221
|
||||
f 221 233 234
|
||||
f 221 234 222
|
||||
f 222 234 235
|
||||
f 222 235 223
|
||||
f 223 235 236
|
||||
f 223 236 224
|
||||
f 224 236 237
|
||||
f 224 237 225
|
||||
f 225 237 238
|
||||
f 225 238 226
|
||||
f 226 238 239
|
||||
f 226 239 227
|
||||
f 227 239 240
|
||||
f 227 240 228
|
||||
f 228 240 229
|
||||
f 228 229 217
|
||||
f 229 241 242
|
||||
f 229 242 230
|
||||
f 230 242 243
|
||||
f 230 243 231
|
||||
f 231 243 244
|
||||
f 231 244 232
|
||||
f 232 244 245
|
||||
f 232 245 233
|
||||
f 233 245 246
|
||||
f 233 246 234
|
||||
f 234 246 247
|
||||
f 234 247 235
|
||||
f 235 247 248
|
||||
f 235 248 236
|
||||
f 236 248 249
|
||||
f 236 249 237
|
||||
f 237 249 250
|
||||
f 237 250 238
|
||||
f 238 250 251
|
||||
f 238 251 239
|
||||
f 239 251 252
|
||||
f 239 252 240
|
||||
f 240 252 241
|
||||
f 240 241 229
|
||||
f 241 253 254
|
||||
f 241 254 242
|
||||
f 242 254 255
|
||||
f 242 255 243
|
||||
f 243 255 256
|
||||
f 243 256 244
|
||||
f 244 256 257
|
||||
f 244 257 245
|
||||
f 245 257 258
|
||||
f 245 258 246
|
||||
f 246 258 259
|
||||
f 246 259 247
|
||||
f 247 259 260
|
||||
f 247 260 248
|
||||
f 248 260 261
|
||||
f 248 261 249
|
||||
f 249 261 262
|
||||
f 249 262 250
|
||||
f 250 262 263
|
||||
f 250 263 251
|
||||
f 251 263 264
|
||||
f 251 264 252
|
||||
f 252 264 253
|
||||
f 252 253 241
|
||||
f 253 265 266
|
||||
f 253 266 254
|
||||
f 254 266 267
|
||||
f 254 267 255
|
||||
f 255 267 268
|
||||
f 255 268 256
|
||||
f 256 268 269
|
||||
f 256 269 257
|
||||
f 257 269 270
|
||||
f 257 270 258
|
||||
f 258 270 271
|
||||
f 258 271 259
|
||||
f 259 271 272
|
||||
f 259 272 260
|
||||
f 260 272 273
|
||||
f 260 273 261
|
||||
f 261 273 274
|
||||
f 261 274 262
|
||||
f 262 274 275
|
||||
f 262 275 263
|
||||
f 263 275 276
|
||||
f 263 276 264
|
||||
f 264 276 265
|
||||
f 264 265 253
|
||||
f 265 277 278
|
||||
f 265 278 266
|
||||
f 266 278 279
|
||||
f 266 279 267
|
||||
f 267 279 280
|
||||
f 267 280 268
|
||||
f 268 280 281
|
||||
f 268 281 269
|
||||
f 269 281 282
|
||||
f 269 282 270
|
||||
f 270 282 283
|
||||
f 270 283 271
|
||||
f 271 283 284
|
||||
f 271 284 272
|
||||
f 272 284 285
|
||||
f 272 285 273
|
||||
f 273 285 286
|
||||
f 273 286 274
|
||||
f 274 286 287
|
||||
f 274 287 275
|
||||
f 275 287 288
|
||||
f 275 288 276
|
||||
f 276 288 277
|
||||
f 276 277 265
|
||||
f 277 1 2
|
||||
f 277 2 278
|
||||
f 278 2 3
|
||||
f 278 3 279
|
||||
f 279 3 4
|
||||
f 279 4 280
|
||||
f 280 4 5
|
||||
f 280 5 281
|
||||
f 281 5 6
|
||||
f 281 6 282
|
||||
f 282 6 7
|
||||
f 282 7 283
|
||||
f 283 7 8
|
||||
f 283 8 284
|
||||
f 284 8 9
|
||||
f 284 9 285
|
||||
f 285 9 10
|
||||
f 285 10 286
|
||||
f 286 10 11
|
||||
f 286 11 287
|
||||
f 287 11 12
|
||||
f 287 12 288
|
||||
f 288 12 1
|
||||
f 288 1 277
|
||||
@@ -0,0 +1,172 @@
|
||||
# Low-poly watertight glass shell with an open neck. Y up, metres.
|
||||
o WhiskeyBottle
|
||||
v 0.5500 0.0000 0.0000
|
||||
v 0.3889 0.0000 0.3889
|
||||
v 0.0000 0.0000 0.5500
|
||||
v -0.3889 0.0000 0.3889
|
||||
v -0.5500 0.0000 0.0000
|
||||
v -0.3889 0.0000 -0.3889
|
||||
v 0.0000 0.0000 -0.5500
|
||||
v 0.3889 0.0000 -0.3889
|
||||
v 0.5500 1.2000 0.0000
|
||||
v 0.3889 1.2000 0.3889
|
||||
v 0.0000 1.2000 0.5500
|
||||
v -0.3889 1.2000 0.3889
|
||||
v -0.5500 1.2000 0.0000
|
||||
v -0.3889 1.2000 -0.3889
|
||||
v 0.0000 1.2000 -0.5500
|
||||
v 0.3889 1.2000 -0.3889
|
||||
v 0.3500 1.5000 0.0000
|
||||
v 0.2475 1.5000 0.2475
|
||||
v 0.0000 1.5000 0.3500
|
||||
v -0.2475 1.5000 0.2475
|
||||
v -0.3500 1.5000 0.0000
|
||||
v -0.2475 1.5000 -0.2475
|
||||
v 0.0000 1.5000 -0.3500
|
||||
v 0.2475 1.5000 -0.2475
|
||||
v 0.2800 1.7000 0.0000
|
||||
v 0.1980 1.7000 0.1980
|
||||
v 0.0000 1.7000 0.2800
|
||||
v -0.1980 1.7000 0.1980
|
||||
v -0.2800 1.7000 0.0000
|
||||
v -0.1980 1.7000 -0.1980
|
||||
v 0.0000 1.7000 -0.2800
|
||||
v 0.1980 1.7000 -0.1980
|
||||
v 0.2800 2.1000 0.0000
|
||||
v 0.1980 2.1000 0.1980
|
||||
v 0.0000 2.1000 0.2800
|
||||
v -0.1980 2.1000 0.1980
|
||||
v -0.2800 2.1000 0.0000
|
||||
v -0.1980 2.1000 -0.1980
|
||||
v 0.0000 2.1000 -0.2800
|
||||
v 0.1980 2.1000 -0.1980
|
||||
v 0.1800 2.1000 0.0000
|
||||
v 0.1273 2.1000 0.1273
|
||||
v 0.0000 2.1000 0.1800
|
||||
v -0.1273 2.1000 0.1273
|
||||
v -0.1800 2.1000 0.0000
|
||||
v -0.1273 2.1000 -0.1273
|
||||
v 0.0000 2.1000 -0.1800
|
||||
v 0.1273 2.1000 -0.1273
|
||||
v 0.1800 1.6500 0.0000
|
||||
v 0.1273 1.6500 0.1273
|
||||
v 0.0000 1.6500 0.1800
|
||||
v -0.1273 1.6500 0.1273
|
||||
v -0.1800 1.6500 0.0000
|
||||
v -0.1273 1.6500 -0.1273
|
||||
v 0.0000 1.6500 -0.1800
|
||||
v 0.1273 1.6500 -0.1273
|
||||
v 0.2700 1.4200 0.0000
|
||||
v 0.1909 1.4200 0.1909
|
||||
v 0.0000 1.4200 0.2700
|
||||
v -0.1909 1.4200 0.1909
|
||||
v -0.2700 1.4200 0.0000
|
||||
v -0.1909 1.4200 -0.1909
|
||||
v 0.0000 1.4200 -0.2700
|
||||
v 0.1909 1.4200 -0.1909
|
||||
v 0.4500 1.1500 0.0000
|
||||
v 0.3182 1.1500 0.3182
|
||||
v 0.0000 1.1500 0.4500
|
||||
v -0.3182 1.1500 0.3182
|
||||
v -0.4500 1.1500 0.0000
|
||||
v -0.3182 1.1500 -0.3182
|
||||
v 0.0000 1.1500 -0.4500
|
||||
v 0.3182 1.1500 -0.3182
|
||||
v 0.4500 0.1200 0.0000
|
||||
v 0.3182 0.1200 0.3182
|
||||
v 0.0000 0.1200 0.4500
|
||||
v -0.3182 0.1200 0.3182
|
||||
v -0.4500 0.1200 0.0000
|
||||
v -0.3182 0.1200 -0.3182
|
||||
v 0.0000 0.1200 -0.4500
|
||||
v 0.3182 0.1200 -0.3182
|
||||
v 0.0000 0.1200 0.0000
|
||||
v 0.0000 0.0000 0.0000
|
||||
f 1 2 10 9
|
||||
f 2 3 11 10
|
||||
f 3 4 12 11
|
||||
f 4 5 13 12
|
||||
f 5 6 14 13
|
||||
f 6 7 15 14
|
||||
f 7 8 16 15
|
||||
f 8 1 9 16
|
||||
f 9 10 18 17
|
||||
f 10 11 19 18
|
||||
f 11 12 20 19
|
||||
f 12 13 21 20
|
||||
f 13 14 22 21
|
||||
f 14 15 23 22
|
||||
f 15 16 24 23
|
||||
f 16 9 17 24
|
||||
f 17 18 26 25
|
||||
f 18 19 27 26
|
||||
f 19 20 28 27
|
||||
f 20 21 29 28
|
||||
f 21 22 30 29
|
||||
f 22 23 31 30
|
||||
f 23 24 32 31
|
||||
f 24 17 25 32
|
||||
f 25 26 34 33
|
||||
f 26 27 35 34
|
||||
f 27 28 36 35
|
||||
f 28 29 37 36
|
||||
f 29 30 38 37
|
||||
f 30 31 39 38
|
||||
f 31 32 40 39
|
||||
f 32 25 33 40
|
||||
f 41 42 50 49
|
||||
f 42 43 51 50
|
||||
f 43 44 52 51
|
||||
f 44 45 53 52
|
||||
f 45 46 54 53
|
||||
f 46 47 55 54
|
||||
f 47 48 56 55
|
||||
f 48 41 49 56
|
||||
f 49 50 58 57
|
||||
f 50 51 59 58
|
||||
f 51 52 60 59
|
||||
f 52 53 61 60
|
||||
f 53 54 62 61
|
||||
f 54 55 63 62
|
||||
f 55 56 64 63
|
||||
f 56 49 57 64
|
||||
f 57 58 66 65
|
||||
f 58 59 67 66
|
||||
f 59 60 68 67
|
||||
f 60 61 69 68
|
||||
f 61 62 70 69
|
||||
f 62 63 71 70
|
||||
f 63 64 72 71
|
||||
f 64 57 65 72
|
||||
f 65 66 74 73
|
||||
f 66 67 75 74
|
||||
f 67 68 76 75
|
||||
f 68 69 77 76
|
||||
f 69 70 78 77
|
||||
f 70 71 79 78
|
||||
f 71 72 80 79
|
||||
f 72 65 73 80
|
||||
f 33 34 42 41
|
||||
f 34 35 43 42
|
||||
f 35 36 44 43
|
||||
f 36 37 45 44
|
||||
f 37 38 46 45
|
||||
f 38 39 47 46
|
||||
f 39 40 48 47
|
||||
f 40 33 41 48
|
||||
f 82 2 1
|
||||
f 82 3 2
|
||||
f 82 4 3
|
||||
f 82 5 4
|
||||
f 82 6 5
|
||||
f 82 7 6
|
||||
f 82 8 7
|
||||
f 82 1 8
|
||||
f 81 73 74
|
||||
f 81 74 75
|
||||
f 81 75 76
|
||||
f 81 76 77
|
||||
f 81 77 78
|
||||
f 81 78 79
|
||||
f 81 79 80
|
||||
f 81 80 73
|
||||
@@ -0,0 +1,82 @@
|
||||
// Opt-in bounded solid attachment check, no rendering inside the loop.
|
||||
function verifyVolumeFluidAttachments() {
|
||||
rebuild(11);
|
||||
advanceAttachment(0.0);
|
||||
local initial = checked(solver.snapshot());
|
||||
if (initial.version != 20 || initial.attachments.len() != 16) throw "Missing solid attachments";
|
||||
for (local frame = 1; frame <= 60; ++frame) advanceAttachment(frame / 60.0);
|
||||
local final = checked(solver.snapshot());
|
||||
if (final.attachments.len() != 16) throw "Attachment count changed";
|
||||
local angle = 0.35 * sin(1.0);
|
||||
foreach (anchor in final.attachments) {
|
||||
local localPoint = anchor.localPosition;
|
||||
local p = final.particles[anchor.particleIndex];
|
||||
if (fabs(p.color[0] - 0.95) > 0.00001 || fabs(p.color[1] - 0.65) > 0.00001 || fabs(p.color[2] - 0.2) > 0.00001)
|
||||
throw "Solid color not applied";
|
||||
local expectedX = 0.3 * sin(1.0) + cos(angle)*localPoint[0] - sin(angle)*localPoint[1];
|
||||
local expectedY = 0.5 + sin(angle)*localPoint[0] + cos(angle)*localPoint[1];
|
||||
if (fabs(p.position[0]-expectedX) > 0.00001 || fabs(p.position[1]-expectedY) > 0.00001)
|
||||
throw "Solid attachment lost local point";
|
||||
local localOrientation = anchor.localOrientation;
|
||||
local expectedZ = sin(angle * 0.5) * localOrientation[3] + cos(angle * 0.5) * localOrientation[2];
|
||||
local expectedW = cos(angle * 0.5) * localOrientation[3] - sin(angle * 0.5) * localOrientation[2];
|
||||
if (fabs(p.orientation[2]-expectedZ) > 0.00001 || fabs(p.orientation[3]-expectedW) > 0.00001)
|
||||
throw "Solid attachment lost local orientation";
|
||||
}
|
||||
checked(solver.restore(final));
|
||||
if (checked(solver.snapshot()).attachments.len() != 16) throw "Attachment restore failed";
|
||||
|
||||
local emptyDefinition=checked(fluids.volumeDefaults());
|
||||
emptyDefinition.settings.capacity=final.settings.capacity;
|
||||
local colliderFirst=checked(fluids.newVolumeSimulator(emptyDefinition));
|
||||
checked(colliderFirst.restore(final));
|
||||
checked(colliderFirst.setColliders([]));
|
||||
local withoutCollider=checked(colliderFirst.snapshot());
|
||||
if(withoutCollider.attachments.len()!=0||withoutCollider.particles.len()!=final.particles.len())
|
||||
throw "Collider-first destruction left stale attachments or removed particles";
|
||||
|
||||
local particlesFirst=checked(fluids.newVolumeSimulator(emptyDefinition));
|
||||
checked(particlesFirst.restore(final));
|
||||
particlesFirst.clear();
|
||||
local withoutParticles=checked(particlesFirst.snapshot());
|
||||
if(withoutParticles.attachments.len()!=0||withoutParticles.particles.len()!=0)
|
||||
throw "Particle-first destruction left stale attachments";
|
||||
checked(particlesFirst.setColliders([]));
|
||||
paused = true; dirty = true;
|
||||
return "VOLUME_SOLID_ATTACHMENT_PASS attached="+final.attachments.len()+" colliderFirst=0 particleFirst=0";
|
||||
}
|
||||
|
||||
// Seed from an actual collider contact, then freeze one adjoining layer per call.
|
||||
function verifyVolumeFluidPropagation() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
prototype.material.viscosity = 0.0; prototype.material.cohesion = 0.0;
|
||||
for (local i = 0; i < 4; ++i) {
|
||||
local p = clone prototype;
|
||||
p.position = [0.0, 1.2999 + i * 0.1, 0.0];
|
||||
definition.particles.append(p);
|
||||
}
|
||||
local sim = checked(fluids.newVolumeSimulator(definition));
|
||||
local collider = checked(fluids.volumeColliderDefaults());
|
||||
collider.label = 17; collider.solidify = true;
|
||||
checked(sim.setColliders([collider]));
|
||||
for (local wave = 0; wave < 3; ++wave) {
|
||||
checked(sim.step(1.0 / 120.0, 1));
|
||||
checked(sim.applySolidColor(0.95, 0.65, 0.2, 1.0));
|
||||
if (checked(sim.snapshot()).attachments.len() != wave + 2)
|
||||
throw "Solid contact propagation did not advance one layer";
|
||||
}
|
||||
collider.center[0] = 0.1;
|
||||
checked(sim.setColliders([collider]));
|
||||
checked(sim.step(1.0 / 120.0, 1));
|
||||
foreach (p in checked(sim.snapshot()).particles)
|
||||
if (fabs(p.position[0] - 0.1) > 0.00001) throw "Propagated solid did not follow collider";
|
||||
return "VOLUME_SOLID_PROPAGATION_PASS waves=4 moved=4 colored=4";
|
||||
}
|
||||
|
||||
function verifySolidifyOnContact() {
|
||||
local attachment=verifyVolumeFluidAttachments();
|
||||
local propagation=verifyVolumeFluidPropagation();
|
||||
return attachment+" | "+propagation;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// WhiskeyBottle helpers. The mesh SDF is baked once in fluidBottleScene(); frame updates change only its pose.
|
||||
const FLUID_BOTTLE_PIVOT_Y = 0.65;
|
||||
const FLUID_BOTTLE_BASE_Y = 0.15;
|
||||
|
||||
function fluidBottlePose(angle, angularVelocity = 0.0) {
|
||||
local c=cos(angle); local s=sin(angle);
|
||||
local pose=checked(fluids.volumeSdfPoseDefaults());
|
||||
pose.label=301;
|
||||
pose.position=[s*FLUID_BOTTLE_PIVOT_Y,
|
||||
FLUID_BOTTLE_BASE_Y+FLUID_BOTTLE_PIVOT_Y-c*FLUID_BOTTLE_PIVOT_Y,0.0];
|
||||
pose.rotation=[0.0,0.0,sin(angle*0.5),cos(angle*0.5)];
|
||||
pose.angularVelocity=[0.0,0.0,angularVelocity];
|
||||
return pose;
|
||||
}
|
||||
|
||||
function fluidBottlePoint(localPoint,angle) {
|
||||
local pose=fluidBottlePose(angle); local c=cos(angle); local s=sin(angle);
|
||||
return [pose.position[0]+c*localPoint[0]-s*localPoint[1],
|
||||
pose.position[1]+s*localPoint[0]+c*localPoint[1],localPoint[2]];
|
||||
}
|
||||
|
||||
function fluidBottleScene() {
|
||||
local definition=checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity=48; definition.settings.spacing=0.09;
|
||||
definition.settings.minimum=[-1.8,-0.25,-0.65]; definition.settings.maximum=[1.8,2.5,0.65];
|
||||
definition.settings.iterations=3;
|
||||
local prototype=checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for(local z=0;z<2;++z) for(local y=0;y<3;++y) for(local x=0;x<4;++x) {
|
||||
local p=clone prototype; p.material=clone prototype.material;
|
||||
p.material.viscosity=0.8; p.material.cohesion=0.35;
|
||||
p.position=[-0.18+x*0.12,FLUID_BOTTLE_BASE_Y+0.22+y*0.12,-0.06+z*0.12];
|
||||
p.color=[0.78,0.28,0.035,0.92]; definition.particles.append(p);
|
||||
}
|
||||
local model=model3d.newModelDataFromFile("assets/whiskey-bottle.obj");
|
||||
local collider=checked(fluids.volumeSdfColliderFromModel(model,0,1.0,1.0,1.0,24));
|
||||
collider.label=301; collider.friction=0.04;
|
||||
local pose=fluidBottlePose(0.0); collider.position=pose.position; collider.rotation=pose.rotation;
|
||||
definition.sdfColliders=[collider];
|
||||
return {solver=checked(fluids.newVolumeSimulator(definition)),angle=0.0,pose=pose};
|
||||
}
|
||||
|
||||
function fluidBottleOutsideCavity(particle,angle) {
|
||||
local pose=fluidBottlePose(angle); local dx=particle.position[0]-pose.position[0];
|
||||
local dy=particle.position[1]-pose.position[1]; local c=cos(angle); local s=sin(angle);
|
||||
local x=c*dx+s*dy; local y=-s*dx+c*dy; local radial=sqrt(x*x+particle.position[2]*particle.position[2]);
|
||||
if(y<0.08) return true;
|
||||
if(y<1.14) return radial>0.47;
|
||||
if(y<1.46) return radial>(0.47-(y-1.14)*0.75);
|
||||
return radial>0.20;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Opt-in FaucetAndBucket acceptance. Loading this file does not run it.
|
||||
function verifyVolumeFluidBucket() {
|
||||
paused=true;rebuild(16);
|
||||
local emittedTotal=0;local emittedAfterLifetime=0;local peakActive=0;
|
||||
local peakContacts=0;local uprightOutside=0;local overflowObserved=0;
|
||||
for(local frame=0;frame<360;++frame) {
|
||||
if(frame>=150) {
|
||||
local next=bucket.angle+0.012;
|
||||
if(next>1.45)next=1.45;
|
||||
if(next!=bucket.angle) {
|
||||
bucket.angle=next;
|
||||
fluidBucketUpdateColliderPoses(bucket.walls,bucket.colliders,bucket.angle);
|
||||
checked(solver.setColliders(bucket.colliders));
|
||||
}
|
||||
}
|
||||
checked(solver.step(1.0/120.0,1));
|
||||
local emitted=checked(emitter.advance(solver,nozzle,1.0/120.0,2,0.25));
|
||||
emittedTotal+=emitted;if(frame>230)emittedAfterLifetime+=emitted;
|
||||
local active=solver.getParticleCount();if(active>peakActive)peakActive=active;
|
||||
local contacts=checked(solver.contacts()).len();if(contacts>peakContacts)peakContacts=contacts;
|
||||
if(frame==145 || frame>240) {
|
||||
local outside=0;
|
||||
foreach(particle in checked(solver.snapshot()).particles)
|
||||
if(fluidBucketOutside(particle,bucket.angle))++outside;
|
||||
if(frame==145)uprightOutside=outside;
|
||||
if(outside>overflowObserved)overflowObserved=outside;
|
||||
}
|
||||
}
|
||||
bucket.emitted=emittedTotal;bucket.peakActive=peakActive;dirty=true;
|
||||
if(emittedTotal<=peakActive || emittedAfterLifetime<=0 || peakActive>=64)
|
||||
throw "Bucket emission pool did not remain bounded and reusable";
|
||||
if(peakContacts<=0 || bucket.angle<1.4)
|
||||
throw "Moving bucket did not produce contacts at its poured pose";
|
||||
if(uprightOutside!=0 || overflowObserved<=0)
|
||||
throw "Bucket did not retain upright fluid then overflow while tilted";
|
||||
return "VOLUME_FLUID_BUCKET_PASS emitted="+emittedTotal+" peak="+peakActive+
|
||||
" recycled="+(emittedTotal-peakActive)+" afterLifetime="+emittedAfterLifetime+
|
||||
" contacts="+peakContacts+" overflow="+overflowObserved;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Shared FaucetAndBucket construction helpers. Loading this file is inert.
|
||||
function fluidBucketPoint(localPoint, angle) {
|
||||
local c=cos(angle);local s=sin(angle);
|
||||
return [c*localPoint[0]-s*localPoint[1],0.72+s*localPoint[0]+c*localPoint[1],localPoint[2]];
|
||||
}
|
||||
|
||||
function fluidBucketWalls() {
|
||||
return [
|
||||
{center=[-0.55,0.0,0.0],half=[0.06,0.60,0.36],label=201},
|
||||
{center=[0.55,0.0,0.0],half=[0.06,0.60,0.36],label=202},
|
||||
{center=[0.0,-0.55,0.0],half=[0.60,0.06,0.36],label=203}
|
||||
];
|
||||
}
|
||||
|
||||
function fluidBucketUpdateColliderPoses(walls,colliders,angle) {
|
||||
local rotation=[0.0,0.0,sin(angle*0.5),cos(angle*0.5)];
|
||||
for(local i=0;i<walls.len();++i) {
|
||||
colliders[i].center=fluidBucketPoint(walls[i].center,angle);
|
||||
colliders[i].rotation=rotation;
|
||||
}
|
||||
}
|
||||
|
||||
function fluidBucketColliders(walls,angle) {
|
||||
local colliders=[];
|
||||
foreach(wall in walls) {
|
||||
local collider=checked(fluids.volumeColliderDefaults());
|
||||
collider.label=wall.label;collider.shape=1;collider.halfExtent=wall.half;
|
||||
collider.friction=0.12;colliders.append(collider);
|
||||
}
|
||||
fluidBucketUpdateColliderPoses(walls,colliders,angle);
|
||||
return colliders;
|
||||
}
|
||||
|
||||
function fluidBucketScene() {
|
||||
local definition=checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity=64;definition.settings.spacing=0.09;
|
||||
definition.settings.minimum=[-1.6,-0.25,-0.45];definition.settings.maximum=[1.6,2.5,0.45];
|
||||
definition.settings.iterations=3;
|
||||
local walls=fluidBucketWalls();local colliders=fluidBucketColliders(walls,0.0);
|
||||
definition.colliders=colliders;
|
||||
return {solver=checked(fluids.newVolumeSimulator(definition)),walls=walls,colliders=colliders,
|
||||
angle=0.0,emitted=0,peakActive=0};
|
||||
}
|
||||
|
||||
function fluidBucketOutside(particle,angle) {
|
||||
local dx=particle.position[0];local dy=particle.position[1]-0.72;
|
||||
local c=cos(angle);local s=sin(angle);
|
||||
local x=c*dx+s*dy;local y=-s*dx+c*dy;
|
||||
return fabs(x)>0.62 || y < -0.64 || (fabs(x)>0.48 && y>0.48);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Opt-in check for Fluid3DEmitter EmissionMethod.BURST lifecycle behavior.
|
||||
function verifyVolumeFluidBurstMode() {
|
||||
local defaults = fluids.volumeDefaults();
|
||||
if (!defaults.ok) throw defaults.error.message;
|
||||
defaults.value.settings.capacity = 8;
|
||||
defaults.value.settings.gravity = [0.0, 0.0, 0.0];
|
||||
local created = fluids.newVolumeSimulator(defaults.value);
|
||||
if (!created.ok) throw created.error.message;
|
||||
local emission = fluids.volumeEmissionDefaults();
|
||||
if (!emission.ok) throw emission.error.message;
|
||||
emission.value.description.prototype.actorGroup = 9;
|
||||
emission.value.description.prototype.life = 0.01;
|
||||
local emitter = eve.VolumeFluidEmitter();
|
||||
local first = emitter.advanceBurst(created.value, emission.value, 2, 0.0);
|
||||
if (!first.ok || first.value != 2) throw "Initial burst failed";
|
||||
local waiting = emitter.advanceBurst(created.value, emission.value, 2, 0.0);
|
||||
if (!waiting.ok || waiting.value != 0) throw "Burst repeated while actor particles were alive";
|
||||
local killed = created.value.killParticle(1);
|
||||
if (!killed.ok) throw killed.error.message;
|
||||
killed = created.value.killParticle(0);
|
||||
if (!killed.ok) throw killed.error.message;
|
||||
local repeated = emitter.advanceBurst(created.value, emission.value, 2, 0.0);
|
||||
if (!repeated.ok || repeated.value != 2) throw "Burst did not resume after actor particles expired";
|
||||
if (emitter.advanceBurst(created.value, emission.value, 0, 0.0).ok)
|
||||
throw "Invalid burst count accepted";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Opt-in Fluid3D-compatible analytic capsule collision check.
|
||||
function verifyVolumeFluidCapsuleCollider() {
|
||||
local definition=checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity=[0.0,0.0,0.0];
|
||||
definition.settings.minimum=[-1.0,-1.0,-1.0];
|
||||
definition.settings.maximum=[1.0,1.0,1.0];
|
||||
local solver=checked(fluids.newVolumeSimulator(definition));
|
||||
local collider=checked(fluids.volumeColliderDefaults());
|
||||
collider.label=81;collider.shape=2;collider.center=[0.0,0.0,0.0];
|
||||
collider.radius=0.1;collider.halfExtent=[0.1,0.3,0.1];
|
||||
collider.rotation=[0.0,0.0,0.70710678,0.70710678];
|
||||
collider.angularVelocity=[0.0,0.0,4.0];collider.friction=1.0;
|
||||
checked(solver.setColliders([collider]));
|
||||
local particle=checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position=[0.15,0.11,0.0];particle.material.cohesion=0.0;particle.material.viscosity=0.0;
|
||||
checked(solver.emit([particle]));checked(solver.step(1.0/120.0,1));
|
||||
local contacts=checked(solver.contacts());local state=checked(solver.snapshot());
|
||||
if(contacts.len()!=1||contacts[0].colliderLabel!=81||contacts[0].normal[1]<0.99||
|
||||
state.particles[0].position[1]<0.1499||state.colliders[0].shape!=2)
|
||||
throw "Rotated capsule projection or codec mismatch";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
definition.particles = [];
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
local particle = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.2, 0.5, 0.0];
|
||||
checked(localSolver.emit([particle]));
|
||||
local trigger = checked(fluids.volumeColliderDefaults());
|
||||
trigger.label = 81;
|
||||
trigger.center = [0.0, 0.5, 0.0];
|
||||
trigger.radius = 0.5;
|
||||
trigger.isTrigger = true;
|
||||
checked(localSolver.setColliders([trigger]));
|
||||
checked(localSolver.step(1.0 / 120.0, 1));
|
||||
local contacts = checked(localSolver.contacts());
|
||||
local state = checked(localSolver.snapshot());
|
||||
if (contacts.len() != 1) throw "trigger contact count: " + contacts.len();
|
||||
if (contacts[0].colliderLabel != 81) throw "trigger contact label mismatch";
|
||||
if (abs(state.particles[0].position[0] - 0.2) > 0.000001)
|
||||
throw "trigger enforced collision projection";
|
||||
::colliderTriggerPass <- true;
|
||||
@@ -0,0 +1,29 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
definition.settings.iterations = 1;
|
||||
definition.particles = [];
|
||||
local solver = checked(fluids.newVolumeSimulator(definition));
|
||||
|
||||
local particle = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.56, 1.0, 0.0];
|
||||
particle.material.stickiness = 0.0;
|
||||
particle.material.stickDistance = 0.1;
|
||||
particle.material.stickinessCombine = 1;
|
||||
checked(solver.emit([particle]));
|
||||
|
||||
local sphere = checked(fluids.volumeColliderDefaults());
|
||||
sphere.label = 91;
|
||||
sphere.center = [0.0, 1.0, 0.0];
|
||||
sphere.radius = 0.5;
|
||||
sphere.stickiness = 10.0;
|
||||
sphere.stickDistance = 0.1;
|
||||
sphere.stickinessCombine = 3;
|
||||
checked(solver.setColliders([sphere]));
|
||||
checked(solver.step(1.0 / 120.0, 1));
|
||||
|
||||
local state = checked(solver.snapshot());
|
||||
local contacts = checked(solver.contacts());
|
||||
if (state.version != 20) throw "collision material snapshot version mismatch";
|
||||
if (state.particles[0].position[0] >= 0.56) throw "maximum-priority adhesion did not pull particle";
|
||||
if (contacts.len() != 1 || contacts[0].colliderLabel != 91) throw "adhesive contact mismatch";
|
||||
::collisionMaterialPass <- true;
|
||||
@@ -0,0 +1,24 @@
|
||||
// Opt-in checks for the reference ColorFromVelocity and ColorFromPhase utilities.
|
||||
function verifyVolumeFluidColorizers() {
|
||||
local solver=checked(fluids.newVolumeSimulator(checked(fluids.volumeDefaults())));
|
||||
local prototype=checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
local a=clone prototype;local b=clone prototype;
|
||||
a.position=[0.0,1.0,0.0];a.velocity=[-0.2,0.0,0.2];a.actorGroup=0;
|
||||
b.position=[0.2,1.0,0.0];b.velocity=[0.2,-0.2,0.0];b.actorGroup=1;
|
||||
checked(solver.emit([a,b]));checked(solver.applyVelocityColors(0.2));
|
||||
local state=checked(solver.snapshot());
|
||||
if(state.particles[0].color[0]>0.001||state.particles[0].color[2]<0.999)
|
||||
throw "ColorFromVelocity mapping mismatch";
|
||||
checked(solver.applyActorGroupColors());state=checked(solver.snapshot());
|
||||
if(state.particles[0].color[0]<0.94||state.particles[1].color[2]<0.86)
|
||||
throw "ColorFromPhase alphabet mismatch";
|
||||
local gradient=[{coordinate=0.0,color=[1.0,0.0,0.0,1.0]},
|
||||
{coordinate=1.0,color=[0.0,0.0,1.0,1.0]}];
|
||||
checked(solver.applyDataColors(0,gradient));state=checked(solver.snapshot());
|
||||
if(state.particles[0].color[0]<0.999)throw "Fluid property colorizer mismatch";
|
||||
checked(solver.applyRandomColors(gradient,91));local first=checked(solver.snapshot());
|
||||
checked(solver.applyRandomColors(gradient,91));local second=checked(solver.snapshot());
|
||||
if(fabs(first.particles[0].color[0]-second.particles[0].color[0])>0.000001)
|
||||
throw "Seeded ColorRandomizer mismatch";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
config = {
|
||||
width = 960
|
||||
height = 720
|
||||
title = "EVEngine | Fluid3D Lab"
|
||||
hotReload = true
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, -10.0, 0.0];
|
||||
definition.settings.iterations = 2;
|
||||
definition.particles = [];
|
||||
local solver = checked(fluids.newVolumeSimulator(definition));
|
||||
|
||||
local particle = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.2, 0.5, 0.0];
|
||||
checked(solver.emit([particle]));
|
||||
|
||||
local sphere = checked(fluids.volumeColliderDefaults());
|
||||
sphere.label = 92;
|
||||
sphere.center = [0.0, 0.5, 0.0];
|
||||
sphere.radius = 0.5;
|
||||
checked(solver.setColliders([sphere]));
|
||||
checked(solver.step(1.0 / 120.0, 1));
|
||||
|
||||
local captured = checked(solver.grabContactParticles(92, 0.0, 0.5, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0, 0.01));
|
||||
if (captured != 1) throw "contact grabber did not capture particle";
|
||||
local before = checked(solver.snapshot()).particles[0].position;
|
||||
local moved = checked(solver.updateGrabbedParticles(92, 0.3, 0.7, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0));
|
||||
if (moved != 1) throw "contact grabber did not update particle";
|
||||
checked(solver.step(1.0 / 120.0, 1));
|
||||
local held = checked(solver.snapshot()).particles[0];
|
||||
if (abs(held.position[0] - before[0] - 0.3) > 0.000001 ||
|
||||
abs(held.position[1] - before[1] - 0.2) > 0.000001)
|
||||
throw "contact grabber local pose was not preserved";
|
||||
if (checked(solver.releaseGrabbedParticles(92)) != 1) throw "contact grabber release mismatch";
|
||||
::contactGrabberPass <- true;
|
||||
@@ -0,0 +1,29 @@
|
||||
// Opt-in check for Fluid3DContactEventDispatcher identity data.
|
||||
function verifyVolumeFluidContactIdentity() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
local particle = clone checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.0, 1.25, 0.0];
|
||||
particle.actorGroup = 23;
|
||||
definition.particles = [particle];
|
||||
local collider = checked(fluids.volumeColliderDefaults());
|
||||
collider.label = 9;
|
||||
definition.colliders = [collider];
|
||||
local source = checked(fluids.newVolumeSimulator(definition));
|
||||
checked(source.step(1.0 / 120.0, 1));
|
||||
local contacts = checked(source.contacts());
|
||||
if (contacts.len() == 0 || contacts[0].colliderLabel != 9 ||
|
||||
contacts[0].particleIndex != 0 || contacts[0].actorGroup != 23)
|
||||
throw "Contact dispatcher identity is incomplete";
|
||||
local tracker = eve.VolumeFluidContactTracker();
|
||||
local entered = checked(tracker.advance(source, 0.01));
|
||||
if (entered.len() != 1 || entered[0].type != 0)
|
||||
throw "Contact enter event is incorrect";
|
||||
local stayed = checked(tracker.advance(source, 0.01));
|
||||
if (stayed.len() != 1 || stayed[0].type != 1)
|
||||
throw "Contact stay event is incorrect";
|
||||
source.clear();
|
||||
local exited = checked(tracker.advance(source, 0.01));
|
||||
if (exited.len() != 1 || exited[0].type != 2)
|
||||
throw "Contact exit event is incorrect";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
function verifyCustomMaterialFastPath() {
|
||||
paused = true;
|
||||
rebuild(0);
|
||||
dirty = false;
|
||||
checked(surface.configureMaterial(true, 0.35, 0.4, 1.25, 0.3, 0.7));
|
||||
checked(surface.configureColors(0.1, 0.2, 0.3, 0.2, 0.7, 0.9));
|
||||
surface.renderVolume(solver);
|
||||
checked(surface.copyToTexture(gfx, texture));
|
||||
checked(surface.renderVolumeColorToTexture(solver, gfx, texture));
|
||||
|
||||
local fullStart = clock();
|
||||
for (local i = 0; i < 20; ++i) {
|
||||
surface.renderVolume(solver);
|
||||
checked(surface.copyToTexture(gfx, texture));
|
||||
}
|
||||
local fullMs = (clock() - fullStart) * 1000.0;
|
||||
local fastStart = clock();
|
||||
for (local i = 0; i < 20; ++i)
|
||||
checked(surface.renderVolumeColorToTexture(solver, gfx, texture));
|
||||
local fastMs = (clock() - fastStart) * 1000.0;
|
||||
if (!surface.usingGpu()) throw "custom material did not use GPU";
|
||||
if (fastMs >= fullMs) throw "device-local custom material path was not faster";
|
||||
return "VOLUME_CUSTOM_MATERIAL_FASTPATH_PASS fullMs=" + fullMs + " fastMs=" + fastMs;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Opt-in secondary-particle contract probe; no simulation/render loop.
|
||||
function verifyVolumeFluidDiffuse() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
local p = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
p.position = [0.0, 1.0, 0.0];
|
||||
p.velocity = [1.0, 0.0, 0.0];
|
||||
definition.particles.append(p);
|
||||
local sim = checked(fluids.newVolumeSimulator(definition));
|
||||
local pool = eve.VolumeFluidDiffuse();
|
||||
local state = checked(pool.snapshot());
|
||||
state.capacity = 1;
|
||||
checked(pool.restore(state));
|
||||
local batch = [{position=[0.0,1.0,0.0], velocity=[0.0,0.0,0.0], life=0.025}];
|
||||
checked(pool.emit(batch));
|
||||
if (pool.emit(batch).ok) throw "Diffuse capacity exceeded";
|
||||
checked(pool.advance(sim, 0.01, 1));
|
||||
local next = checked(pool.snapshot()).particles[0];
|
||||
if (fabs(next.position[0]-0.01)>0.00001 || fabs(next.life-0.015)>0.00001)
|
||||
throw "Diffuse advection or lifetime mismatch";
|
||||
if (checked(sim.snapshot()).particles[0].position[0]!=0.0)
|
||||
throw "Diffuse advanced source fluid";
|
||||
if (pool.advance(sim, 0.01, -1).ok) throw "Invalid threshold accepted";
|
||||
state.extra <- 1;
|
||||
if (pool.restore(state).ok) throw "Unknown diffuse state field accepted";
|
||||
checked(pool.advance(sim, 0.02, 1));
|
||||
if (pool.getParticleCount()!=0 || pool.getAvailableCapacity()!=1)
|
||||
throw "Diffuse lifetime did not recycle capacity";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, -10.0, 0.0];
|
||||
definition.settings.capacity = 2;
|
||||
local particle = clone checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [-0.2, 1.0, 0.0];
|
||||
definition.particles = [particle];
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
local target = checked(fluids.volumeColliderDefaults());
|
||||
target.label = 72; target.center = [0.0, 1.0, 0.0]; target.radius = 0.1;
|
||||
checked(localSolver.setColliders([target]));
|
||||
if (checked(localSolver.bindDynamicParticles([0], 72, 0.0, 1000000000000.0, false)) != 1)
|
||||
throw "Dynamic particle attachment was not installed";
|
||||
target.center = [0.25, 1.0, 0.0];
|
||||
checked(localSolver.setColliders([target]));
|
||||
checked(localSolver.step(1.0 / 120.0, 1));
|
||||
local attached = checked(localSolver.snapshot());
|
||||
if (attached.version != 20 || attached.attachments.len() != 1 ||
|
||||
!attached.attachments[0].dynamic || attached.attachments[0].compliance != 0.0 ||
|
||||
fabs(attached.particles[0].position[0] - 0.05) > 0.00001 ||
|
||||
attached.particles[0].material.phase != 0)
|
||||
throw "Dynamic particle attachment state mismatch";
|
||||
if (checked(localSolver.unbindStaticParticles([0])) != 1)
|
||||
throw "Dynamic particle attachment was not removed";
|
||||
return "VOLUME_DYNAMIC_ATTACHMENT_PASS";
|
||||
@@ -0,0 +1,21 @@
|
||||
// Opt-in check for EmitOnKeyPress/FluidJetController zero-speed behavior.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 16;
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
local emission = checked(fluids.volumeEmissionDefaults());
|
||||
emission.description.shape = 0;
|
||||
emission.description.extent = [0.0, 0.0, 0.0];
|
||||
local stream = eve.VolumeFluidEmitter();
|
||||
if (checked(stream.advance(localSolver, emission, 0.006, 100.0, 1, 0.0)) != 0)
|
||||
throw "Fractional stream emitted too early";
|
||||
emission.description.speed = 0.0;
|
||||
for (local i = 0; i < 20; ++i)
|
||||
if (checked(stream.advance(localSolver, emission, 1.0 / 30.0, 100.0, 1, 0.0)) != 0)
|
||||
throw "Disabled emitter produced particles";
|
||||
if (localSolver.getParticleCount() != 0 || checked(stream.snapshot()).phase != "0")
|
||||
throw "Disabled emitter retained catchup debt";
|
||||
emission.description.speed = 1.0;
|
||||
if (checked(stream.advance(localSolver, emission, 0.006, 100.0, 1, 0.0)) != 0 ||
|
||||
checked(stream.advance(localSolver, emission, 0.006, 100.0, 1, 0.0)) != 1)
|
||||
throw "Re-enabled emitter did not restart from zero credit";
|
||||
return "VOLUME_EMIT_ON_KEY_PASS";
|
||||
@@ -0,0 +1,23 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
definition.settings.capacity = 8;
|
||||
local source = checked(fluids.newVolumeSimulator(definition));
|
||||
local target = checked(fluids.newVolumeSimulator(definition));
|
||||
local emission = checked(fluids.volumeEmissionDefaults());
|
||||
emission.description.shape = 0;
|
||||
emission.description.extent = [0.0, 0.0, 0.0];
|
||||
emission.description.speed = 2.0;
|
||||
emission.description.prototype.actorGroup = 14;
|
||||
local sourceEmitter = eve.VolumeFluidEmitter();
|
||||
local targetEmitter = eve.VolumeFluidEmitter();
|
||||
if (checked(sourceEmitter.advance(source, emission, 0.02, 120.0, 2, 0.0)) != 2)
|
||||
throw "Checkpoint source did not emit";
|
||||
local checkpoint = checked(sourceEmitter.checkpoint(source, emission));
|
||||
emission.description.prototype.actorGroup = 3;
|
||||
checked(targetEmitter.advance(target, emission, 0.02, 60.0, 1, 0.0));
|
||||
local restoredEmission = checked(targetEmitter.restoreCheckpoint(target, checkpoint));
|
||||
local restored = checked(target.snapshot());
|
||||
if (restored.particles.len() != 2 || restored.particles[0].actorGroup != 14 ||
|
||||
restoredEmission.description.prototype.actorGroup != 14 || !targetEmitter.isEmitting())
|
||||
throw "Combined emitter checkpoint mismatch";
|
||||
print("VOLUME_FLUID_EMITTER_CHECKPOINT_PASS\n");
|
||||
@@ -0,0 +1,88 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
local blueprintMetrics = checked(fluids.volumeEmitterBlueprintMetrics(8.0, 1000.0, 2.0));
|
||||
if (abs(blueprintMetrics.particleSize - 0.05) > 0.000001 ||
|
||||
abs(blueprintMetrics.particleMass - 0.125) > 0.000001 ||
|
||||
abs(blueprintMetrics.smoothingRadius - 0.1) > 0.000001)
|
||||
throw "3D emitter blueprint metrics mismatch";
|
||||
local fluidBlueprint = checked(fluids.volumeFluidEmitterBlueprintDefaults());
|
||||
fluidBlueprint.capacity = 8;
|
||||
fluidBlueprint.resolution = 8.0;
|
||||
fluidBlueprint.restDensity = 875.0;
|
||||
fluidBlueprint.smoothing = 3.0;
|
||||
fluidBlueprint.viscosity = 0.7;
|
||||
fluidBlueprint.surfaceTension = 1.5;
|
||||
fluidBlueprint.buoyancy = -0.25;
|
||||
fluidBlueprint.atmosphericDrag = 0.4;
|
||||
fluidBlueprint.atmosphericPressure = 2.5;
|
||||
fluidBlueprint.vorticity = 0.8;
|
||||
fluidBlueprint.diffusion = 0.6;
|
||||
fluidBlueprint.diffusionData = [1.0, 2.0, 3.0, 4.0];
|
||||
local preparedBlueprint = checked(fluids.prepareVolumeFluidEmitterBlueprint3D(fluidBlueprint));
|
||||
if (preparedBlueprint.solver.settings.capacity != 8 ||
|
||||
abs(preparedBlueprint.solver.settings.spacing - 0.05) > 0.000001 ||
|
||||
preparedBlueprint.emission.description.actorCapacity != 8 ||
|
||||
abs(preparedBlueprint.emission.description.prototype.material.density - 875.0) > 0.000001 ||
|
||||
abs(preparedBlueprint.emission.description.prototype.material.cohesion - 1.5) > 0.000001 ||
|
||||
abs(preparedBlueprint.emission.description.prototype.material.drag - 0.4) > 0.000001 ||
|
||||
preparedBlueprint.emission.description.prototype.data[3] != 4.0)
|
||||
throw "3D fluid emitter blueprint application mismatch";
|
||||
local granularBlueprint = checked(fluids.volumeGranularEmitterBlueprintDefaults());
|
||||
granularBlueprint.capacity = 7;
|
||||
granularBlueprint.resolution = 8.0;
|
||||
granularBlueprint.restDensity = 1450.0;
|
||||
granularBlueprint.randomness = 35.0;
|
||||
local preparedGranular = checked(fluids.prepareVolumeGranularEmitterBlueprint3D(granularBlueprint));
|
||||
if (preparedGranular.solver.settings.capacity != 7 ||
|
||||
abs(preparedGranular.solver.settings.spacing - 0.05) > 0.000001 ||
|
||||
preparedGranular.emission.description.actorCapacity != 7 ||
|
||||
preparedGranular.emission.description.granularRadiusRandomness != 35.0 ||
|
||||
preparedGranular.emission.description.prototype.material.phase != 2 ||
|
||||
preparedGranular.emission.description.prototype.material.density != 1450.0 ||
|
||||
preparedGranular.metrics.smoothingRadius != 0.0)
|
||||
throw "3D granular emitter blueprint application mismatch";
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
definition.settings.capacity = 8;
|
||||
local solver = checked(fluids.newVolumeSimulator(definition));
|
||||
local emission = checked(fluids.volumeEmissionDefaults());
|
||||
emission.description.shape = 0;
|
||||
emission.description.extent = [0.0, 0.0, 0.0];
|
||||
emission.description.prototype.actorGroup = 21;
|
||||
emission.description.actorCapacity = 2;
|
||||
local emitter = eve.VolumeFluidEmitter();
|
||||
if (checked(emitter.activeParticleCount(solver, emission)) != 0)
|
||||
throw "Fresh emitter actor count was not zero";
|
||||
checked(emitter.advance(solver, emission, 0.02, 120.0, 2, 0.0));
|
||||
if (checked(emitter.activeParticleCount(solver, emission)) != 2)
|
||||
throw "Emitter active particle count mismatch";
|
||||
if (checked(emitter.advance(solver, emission, 0.02, 120.0, 2, 0.0)) != 0)
|
||||
throw "Emitter exceeded its actor capacity";
|
||||
local other = checked(fluids.volumeEmissionDefaults());
|
||||
other.description.shape = 0;
|
||||
other.description.extent = [0.0, 0.0, 0.0];
|
||||
other.description.prototype.actorGroup = 22;
|
||||
other.description.actorCapacity = 1;
|
||||
local otherEmitter = eve.VolumeFluidEmitter();
|
||||
if (checked(otherEmitter.advance(solver, other, 0.02, 120.0, 2, 0.0)) != 1)
|
||||
throw "Independent emitter actor capacity was not available";
|
||||
if (checked(emitter.activeParticleCount(solver, other)) != 1)
|
||||
throw "Independent emitter actor count mismatch";
|
||||
checked(emitter.killParticle(solver, emission, 0));
|
||||
if (checked(emitter.activeParticleCount(solver, emission)) != 1)
|
||||
throw "Emitter-local particle deletion did not remove exactly one particle";
|
||||
if (checked(emitter.activeParticleCount(solver, other)) != 1)
|
||||
throw "Emitter-local particle deletion crossed actor ownership";
|
||||
local direct = checked(fluids.volumeEmissionDefaults());
|
||||
direct.description.shape = 0;
|
||||
direct.description.extent = [0.0, 0.0, 0.0];
|
||||
direct.description.speed = 2.0;
|
||||
direct.description.actorCapacity = 1;
|
||||
direct.description.prototype.actorGroup = 23;
|
||||
local directEmitter = eve.VolumeFluidEmitter();
|
||||
if (checked(directEmitter.emitParticle(solver, direct, 0.5, 0.02)) != 1)
|
||||
throw "Direct emitter particle was not admitted";
|
||||
if (checked(directEmitter.emitParticle(solver, direct, 0.0, 0.02)) != 0)
|
||||
throw "Direct emitter particle exceeded actor capacity";
|
||||
checked(solver.killActorParticles(21));
|
||||
if (checked(emitter.activeParticleCount(solver, emission)) != 0)
|
||||
throw "Killed emitter actor still reports particles";
|
||||
print("VOLUME_FLUID_EMITTER_COUNT_PASS\n");
|
||||
@@ -0,0 +1,27 @@
|
||||
function checked(result) {
|
||||
if (!result.ok) throw result.error.message;
|
||||
return result.value;
|
||||
}
|
||||
|
||||
local defaults = checked(fluids.volumeDefaults());
|
||||
defaults.settings.gravity = [0.0, 0.0, 0.0];
|
||||
defaults.settings.capacity = 8;
|
||||
local solver = checked(fluids.newVolumeSimulator(defaults));
|
||||
checked(solver.configureParticleEvents(2));
|
||||
|
||||
local emission = checked(fluids.volumeEmissionDefaults());
|
||||
emission.description.prototype.actorGroup = 12;
|
||||
emission.description.prototype.life = 0.01;
|
||||
checked(solver.emitBurst(emission, 2));
|
||||
local emitted = checked(solver.drainParticleEvents());
|
||||
if (emitted.events.len() != 2 || emitted.dropped != 0 ||
|
||||
emitted.events[0].type != 0 || emitted.events[0].actorGroup != 12)
|
||||
throw "Emission lifecycle events mismatch";
|
||||
|
||||
checked(solver.step(0.01, 2));
|
||||
local killed = checked(solver.drainParticleEvents());
|
||||
if (killed.events.len() != 2 || killed.dropped != 0 || killed.events[0].type != 1 ||
|
||||
killed.events[0].particle.actorGroup != 12)
|
||||
throw "Expiry lifecycle events mismatch";
|
||||
|
||||
print("VOLUME_FLUID_EMITTER_EVENTS_PASS\n");
|
||||
@@ -0,0 +1,20 @@
|
||||
// Opt-in check for Fluid3DEmitter.Filter updating every live particle in one actor.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 4;
|
||||
local solver = checked(fluids.newVolumeSimulator(definition));
|
||||
local emission = checked(fluids.volumeEmissionDefaults());
|
||||
emission.description.shape = 0;
|
||||
emission.description.extent = [0.0, 0.0, 0.0];
|
||||
emission.description.prototype.actorGroup = 7;
|
||||
emission.description.prototype.collisionFilter = 983041;
|
||||
checked(solver.emitBurst(emission, 2));
|
||||
checked(solver.setActorCollisionFilter(7, 11141152));
|
||||
local particles = checked(solver.snapshot()).particles;
|
||||
if (particles.len() != 2 || particles[0].collisionFilter != 11141152 ||
|
||||
particles[1].collisionFilter != 11141152)
|
||||
throw "Emitter filter did not update every live actor particle";
|
||||
if (solver.setActorCollisionFilter(7, 11141120).ok)
|
||||
throw "Filter with an empty category was accepted";
|
||||
if (checked(solver.snapshot()).particles[0].collisionFilter != 11141152)
|
||||
throw "Rejected filter partially mutated the actor";
|
||||
return "VOLUME_EMITTER_FILTER_PASS";
|
||||
@@ -0,0 +1,24 @@
|
||||
// Opt-in check for Fluid3DEmitter.KillAll in a shared volume solver.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 8;
|
||||
local solver = checked(fluids.newVolumeSimulator(definition));
|
||||
local first = checked(fluids.volumeEmissionDefaults());
|
||||
first.description.shape = 0;
|
||||
first.description.extent = [0.0, 0.0, 0.0];
|
||||
first.description.prototype.actorGroup = 7;
|
||||
checked(solver.emitBurst(first, 2));
|
||||
local second = checked(fluids.volumeEmissionDefaults());
|
||||
second.description.shape = 0;
|
||||
second.description.origin = [0.5, 1.0, 0.0];
|
||||
second.description.extent = [0.0, 0.0, 0.0];
|
||||
second.description.prototype.actorGroup = 8;
|
||||
checked(solver.emitBurst(second, 3));
|
||||
if (checked(solver.killActorParticles(7)) != 2)
|
||||
throw "Emitter KillAll returned the wrong count";
|
||||
local particles = checked(solver.snapshot()).particles;
|
||||
if (particles.len() != 3) throw "Emitter KillAll removed another actor";
|
||||
foreach (particle in particles)
|
||||
if (particle.actorGroup != 8) throw "Killed actor particle survived";
|
||||
if (solver.killActorParticles(7).ok)
|
||||
throw "Missing actor KillAll was accepted";
|
||||
return "VOLUME_EMITTER_KILL_ALL_PASS";
|
||||
@@ -0,0 +1,33 @@
|
||||
// Opt-in check for Fluid3DEmitter.UpdateParticleMaterial on existing actor particles.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 4;
|
||||
definition.settings.spacing = 0.2;
|
||||
local solver = checked(fluids.newVolumeSimulator(definition));
|
||||
local source = checked(fluids.volumeEmissionDefaults());
|
||||
source.description.shape = 0;
|
||||
source.description.extent = [0.0, 0.0, 0.0];
|
||||
source.description.prototype.actorGroup = 7;
|
||||
source.description.prototype.color = [1.0, 0.0, 0.0, 1.0];
|
||||
source.description.prototype.life = 3.0;
|
||||
checked(solver.emitBurst(source, 2));
|
||||
|
||||
local material = checked(fluids.volumeEmissionDefaults());
|
||||
material.description.prototype.material.viscosity = 9.0;
|
||||
material.description.prototype.material.cohesion = 0.7;
|
||||
material.description.prototype.data = [1.0, 2.0, 3.0, 4.0];
|
||||
material.description.prototype.radii = [0.0, 0.0, 0.0];
|
||||
material.description.prototype.collisionFilter = 11141152;
|
||||
material.description.prototype.selfCollide = false;
|
||||
checked(solver.updateActorMaterial(7, material));
|
||||
foreach (particle in checked(solver.snapshot()).particles) {
|
||||
if (fabs(particle.material.viscosity - 9.0) > 0.00001 ||
|
||||
fabs(particle.radii[0] - 0.1) > 0.00001 || particle.collisionFilter != 11141152 ||
|
||||
particle.selfCollide || particle.color[0] != 1.0 || particle.life != 3.0)
|
||||
throw "Emitter material refresh mismatch";
|
||||
}
|
||||
material.description.prototype.material.viscosity = -1.0;
|
||||
if (solver.updateActorMaterial(7, material).ok)
|
||||
throw "Invalid material refresh was accepted";
|
||||
if (fabs(checked(solver.snapshot()).particles[0].material.viscosity - 9.0) > 0.00001)
|
||||
throw "Rejected material refresh partially mutated actor";
|
||||
return "VOLUME_EMITTER_MATERIAL_PASS";
|
||||
@@ -0,0 +1,18 @@
|
||||
// Opt-in check for Fluid3DEmitter.SetSelfCollisions on existing actor particles.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 4;
|
||||
local solver = checked(fluids.newVolumeSimulator(definition));
|
||||
local emission = checked(fluids.volumeEmissionDefaults());
|
||||
emission.description.shape = 0;
|
||||
emission.description.extent = [0.0, 0.0, 0.0];
|
||||
emission.description.prototype.actorGroup = 7;
|
||||
checked(solver.emitBurst(emission, 2));
|
||||
checked(solver.setActorSelfCollisions(7, false));
|
||||
foreach (particle in checked(solver.snapshot()).particles)
|
||||
if (particle.selfCollide) throw "Actor self-collision disable was not propagated";
|
||||
checked(solver.setActorSelfCollisions(7, true));
|
||||
foreach (particle in checked(solver.snapshot()).particles)
|
||||
if (!particle.selfCollide) throw "Actor self-collision enable was not propagated";
|
||||
if (solver.setActorSelfCollisions(9, false).ok)
|
||||
throw "Missing actor self-collision update was accepted";
|
||||
return "VOLUME_EMITTER_SELF_COLLISION_PASS";
|
||||
@@ -0,0 +1,18 @@
|
||||
// Opt-in check for allocation-free Fluid3DEmitter.isEmitting projection.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 4;
|
||||
local solver = checked(fluids.newVolumeSimulator(definition));
|
||||
local emission = checked(fluids.volumeEmissionDefaults());
|
||||
emission.description.shape = 0;
|
||||
emission.description.extent = [0.0, 0.0, 0.0];
|
||||
local emitter = eve.VolumeFluidEmitter();
|
||||
if (emitter.isEmitting()) throw "Fresh emitter reported active";
|
||||
if (checked(emitter.advance(solver, emission, 0.02, 100.0, 1, 0.0)) != 1)
|
||||
throw "Emitter state probe did not emit";
|
||||
if (!emitter.isEmitting() || emitter.isEmitting() != checked(emitter.snapshot()).emitting)
|
||||
throw "Direct emitter state differs from snapshot";
|
||||
local saved = checked(emitter.snapshot());
|
||||
saved.emitting = false;
|
||||
checked(emitter.restore(saved));
|
||||
if (emitter.isEmitting()) throw "Restored inactive state was ignored";
|
||||
return "VOLUME_EMITTER_STATE_PASS";
|
||||
@@ -0,0 +1 @@
|
||||
{"native":{"anim":[],"card":{"cards":[]},"dialogue":[{"characters":[],"choices":[],"currentLineId":"","fullText":"","globalVars":{},"lipSyncAmplitude":0.8500000238418579,"lipSyncEnabled":true,"lipSyncParameter":"mouthOpen","phase":"idle","rngState":2654435769,"sceneVars":{},"selectedChoiceId":"","slotX":{},"speakerId":"","typeSpeed":40,"typed":0}],"rpg":[]},"roots":{"PI":3.1415927410125732,"RAND_MAX":32767,"_async":{"continuations":[],"draining":false,"eventWaits":[],"generation":3,"maxDrain":1000,"nextId":1,"nextTicks":[],"taskWaits":[],"timers":[],"waits":[]},"_boot_module_slots":["fs","hot","timer","platform_event","win","gfx"],"_charsize_":1,"_floatsize_":4,"_frame_count":15262,"_input_edge_state":{},"_intsize_":8,"_startup_first_frame":true,"_startup_first_present":true,"_startup_t0":0.6039999723434448,"_versionnumber_":310,"accumulator":0,"dirty":false,"displayAccumulator":0.03333333507180214,"droppedTime":0,"eve_module_contract":[{"classes":[],"deps":[],"dir":"common","enabled":true,"layer":-1,"name":"common","optionalDeps":[],"profiles":[],"required":true,"slots":[]},{"classes":[],"deps":["filesystem"],"dir":"cmdline","enabled":true,"layer":-1,"name":"cmdline","optionalDeps":[],"profiles":[],"required":true,"slots":[]},{"classes":[],"deps":["platform_event"],"dir":"devtools","enabled":true,"layer":-1,"name":"devtools","optionalDeps":[],"profiles":["3d"],"required":false,"slots":[]},{"classes":["Math"],"deps":[],"dir":"math","enabled":true,"layer":0,"name":"math","optionalDeps":[],"profiles":["minimal","2d","3d","web"],"required":false,"slots":["math"]},{"classes":[],"deps":[],"dir":"grid","enabled":true,"layer":0,"name":"grid","optionalDeps":[],"profiles":["2d","3d","web"],"required":false,"slots":[]},{"classes":["Agent"],"deps":[],"dir":"agent","enabled":true,"layer":0,"name":"agent","optionalDeps":[],"profiles":["2d","3d","web"],"required":false,"slots":["agent"]},{"classes":[],"deps":[],"dir":"property_access","enabled":true,"layer":0,"name":"property_access","optionalDeps":[],"profiles":["minimal","2d","3d","web"],"required":false,"slots":[]},{"classes":[],"deps":["property_access","schema","transaction"],"dir":"editing","enabled":true,"layer":1,"name":"editing","optionalDeps":[],"profiles":[],"required":false,"slots":[]},{"classes":["DataModule"],"deps":[],"dir":"data","enabled":true,"layer":0,"name":"data","optionalDeps":[],"profiles":["minimal","2d","3d","web"],"required":false,"slots":[]},{"classes":["Filesystem","HotReload"],"deps":[],"dir":"filesystem","enabled":true,"layer":0,"name":"filesystem","optionalDeps":[],"profiles":[],"required":true,"slots":["fs","hot"]},{"classes":[],"deps":["data"],"dir":"asset","enabled":true,"layer":0,"name":"asset","optionalDeps":[],"profiles":["minimal","2d","3d","web"],"required":false,"slots":[]},{"classes":["PlatformEvent"],"deps":[],"dir":"platform_event","enabled":true,"layer":0,"name":"platform_event","optionalDeps":[],"profiles":[],"required":true,"slots":["platform_event"]},{"classes":["Timer"],"deps":[],"dir":"timer","enabled":true,"layer":0,"name":"timer","optionalDeps":[],"profiles":[],"required":true,"slots":["timer"]},{"classes":["HostSystem"],"deps":[],"dir":"system","enabled":true,"layer":0,"name":"system","optionalDeps":[],"profiles":["minimal","2d","3d","web"],"required":false,"slots":["system"]},{"classes":["Profiler"],"deps":[],"dir":"profiler","enabled":true,"layer":0,"name":"profiler","optionalDeps":[],"profiles":["minimal","2d","3d","web"],"required":false,"slots":["profiler"]},{"classes":["Thread"],"deps":[],"dir":"thread","enabled":true,"layer":0,"name":"thread","optionalDeps":[],"profiles":["minimal","2d","3d","web"],"required":false,"slots":["thread"]},{"classes":["Spatial"],"deps":[],"dir":"spatial","enabled":true,"layer":0,"name":"spatial","optionalDeps":[],"profiles":["2d","3d","web"],"required":false,"slots":["spatial"]},{"classes":["Crowd"],"deps":[],"dir":"crowd","enabled":true,"layer":0,"name":"crowd","optionalDeps":[],"profiles":["2d","3d","web"],"required":false,"slots":["crowd"]},{"classes":["PixelWorldModule"],"deps":[],"dir":"pixelworld","enabled":true,"layer":0,"name":"pixelworld","optionalDeps":[],"profiles":["2d","3d","web"],"required":false,"slots":["pixelworld"]},{"classes":["IK"],"deps":[],"dir":"ik","enabled":true,"layer":0,"name":"ik","optionalDeps":[],"profiles":["2d","3d","web"],"required":false,"slots":[]},{"classes":["Editor"],"deps":["asset","editing","property_access","rx","tags","transaction"],"dir":"editor","enabled":true,"layer":6,"name":"editor","optionalDeps":["graphics"],"profiles":["3d","web"],"required":false,"slots":["editor"]},{"classes":["Plugins"],"deps":[],"dir":"plugins","enabled":true,"layer":0,"name":"plugins","optionalDeps":[],"profiles":["3d"],"required":false,"slots":[]},{"classes":["Database"],"deps":[],"dir":"database","enabled":true,"layer":0,"name":"database","optionalDeps":[],"profiles":[],"required":false,"slots":[]},{"classes":[],"deps":["pixelworld","thread"],"dir":"pixelworld/Line truncated
|
||||
@@ -0,0 +1,21 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
definition.settings.capacity = 1;
|
||||
local particle = clone checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.0, 1.0, 0.0]; particle.material.density = 1000.0;
|
||||
definition.particles = [particle];
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
local ambient = checked(fluids.volumeWindZoneDefaults());
|
||||
ambient.direction = [1.0, 0.0, 0.0]; ambient.intensity = 8.0;
|
||||
checked(localSolver.accumulateExternalForceZones([ambient], 0.0));
|
||||
checked(localSolver.step(1.0 / 120.0, 1));
|
||||
local first = checked(localSolver.snapshot()).particles[0].velocity[0];
|
||||
if (first <= 0.0) throw "External force zone did not accelerate particle";
|
||||
checked(localSolver.step(1.0 / 120.0, 1));
|
||||
local second = checked(localSolver.snapshot()).particles[0].velocity[0];
|
||||
if (fabs(second - first) > 0.00001) throw "One-step external force was retained";
|
||||
checked(localSolver.setParticleExternalForces([[0.0, 8.0, 0.0]]));
|
||||
checked(localSolver.step(1.0 / 120.0, 1));
|
||||
if (checked(localSolver.snapshot()).particles[0].velocity[1] <= 0.0)
|
||||
throw "Per-particle external force was not applied";
|
||||
return "VOLUME_EXTERNAL_FORCE_PASS";
|
||||
@@ -0,0 +1,21 @@
|
||||
// Diffuse-particle field contract probe; does not advance or render the solver.
|
||||
function verifyVolumeFluidField() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for (local i = 0; i < 2; ++i) {
|
||||
local p = clone prototype;
|
||||
p.position = [i == 0 ? -0.05 : 0.05, 1.0, 0.0];
|
||||
p.velocity = [0.0, i == 0 ? -1.0 : 1.0, 0.0];
|
||||
definition.particles.append(p);
|
||||
}
|
||||
local sim = checked(fluids.newVolumeSimulator(definition));
|
||||
local samples = checked(sim.sampleField([[0.0,1.0,0.0],[100.0,100.0,100.0]]));
|
||||
if (samples.len()!=2 || samples[0].neighborCount!=2 || fabs(samples[0].vorticity[2]-8.0)>0.0001)
|
||||
throw "Field projection mismatch";
|
||||
if (samples[1].neighborCount!=0 || samples[1].density!=0.0)
|
||||
throw "Empty field support mismatch";
|
||||
if (sim.sampleField([[0.0,1.0]]).ok || sim.sampleField([[0.0,1.0,0.0,9.0]]).ok)
|
||||
throw "Malformed field positions accepted";
|
||||
if (sim.getParticleCount()!=2) throw "Field query changed solver state";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Opt-in check for the package SetCategory actor helper.
|
||||
function verifyVolumeFluidFilterCategory() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
local first = clone checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
first.position = [0.0, 1.0, 0.0];
|
||||
first.actorGroup = 7;
|
||||
first.collisionFilter = 15728641; // 0x00f00001
|
||||
local second = clone first;
|
||||
second.position = [0.2, 1.0, 0.0];
|
||||
second.actorGroup = 8;
|
||||
second.collisionFilter = 267386884; // 0x0ff00004
|
||||
definition.particles = [first, second];
|
||||
local source = checked(fluids.newVolumeSimulator(definition));
|
||||
checked(source.setActorFilterCategory(7, 3));
|
||||
local state = checked(source.snapshot());
|
||||
if (state.particles[0].collisionFilter != 15728648 ||
|
||||
state.particles[1].collisionFilter != 267386884)
|
||||
throw "Actor filter category or mask preservation is incorrect";
|
||||
if (source.setActorFilterCategory(7, 16).ok || source.setActorFilterCategory(9, 1).ok)
|
||||
throw "Invalid actor filter update was accepted";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Opt-in FluidJet acceptance probe. Loading this file has no side effects.
|
||||
function verifyVolumeFluidJet() {
|
||||
paused = true;
|
||||
rebuild(6);
|
||||
|
||||
local emittedTotal = 0;
|
||||
local peakActive = 0;
|
||||
local emittedAfterFirstLifetime = 0;
|
||||
local sawLateralMotion = false;
|
||||
local sawTurnedVelocity = false;
|
||||
for (local frame = 0; frame < 240; ++frame) {
|
||||
local t = frame / 60.0;
|
||||
checked(solver.step(1.0 / 60.0, 2));
|
||||
local emitted = checked(emitter.advanceMoving(solver, nozzle,
|
||||
movingPose(t), movingPose(t + 1.0 / 60.0),
|
||||
1.0 / 60.0, 16, 0.5, 1.0));
|
||||
emittedTotal += emitted;
|
||||
if (frame > 100) emittedAfterFirstLifetime += emitted;
|
||||
|
||||
local state = checked(solver.snapshot());
|
||||
if (state.particles.len() > peakActive) peakActive = state.particles.len();
|
||||
foreach (particle in state.particles) {
|
||||
if (abs(particle.position[0]) > 0.2) sawLateralMotion = true;
|
||||
if (abs(particle.velocity[0]) > 0.15 && abs(particle.velocity[2]) > 0.15)
|
||||
sawTurnedVelocity = true;
|
||||
}
|
||||
}
|
||||
|
||||
local active = checked(solver.snapshot()).particles.len();
|
||||
if (emittedTotal <= peakActive)
|
||||
throw "Jet did not reuse expired particle capacity";
|
||||
if (emittedAfterFirstLifetime <= 0)
|
||||
throw "Jet did not resume emission after its first particle lifetime";
|
||||
if (active <= 0 || active > peakActive || peakActive >= 512)
|
||||
throw "Jet active-particle pool was not bounded";
|
||||
if (!sawLateralMotion || !sawTurnedVelocity)
|
||||
throw "Moving/rotating nozzle was not observable in emitted particles";
|
||||
|
||||
return "VOLUME_FLUID_JET_PASS emitted=" + emittedTotal +
|
||||
" peak=" + peakActive + " active=" + active +
|
||||
" recycled=" + (emittedTotal - peakActive);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Opt-in check for Fluid3DFoamGenerator's owning-emitter source restriction.
|
||||
function verifyVolumeFluidFoamActorSource() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for (local i=0; i<4; ++i) {
|
||||
local particle = clone prototype;
|
||||
local right = i >= 2;
|
||||
local upper = (i % 2) == 1;
|
||||
particle.position = [right ? 0.55 : -0.55, upper ? 1.05 : 0.95, 0.0];
|
||||
particle.velocity = [upper ? 1.0 : -1.0, 0.0, 0.0];
|
||||
particle.actorGroup = right ? 9 : 3;
|
||||
definition.particles.append(particle);
|
||||
}
|
||||
local source = checked(fluids.newVolumeSimulator(definition));
|
||||
local foam = eve.VolumeFluidFoam();
|
||||
local state = checked(foam.snapshot());
|
||||
state.settings.rate = 100.0;
|
||||
state.settings.randomness = 0.0;
|
||||
state.settings.vorticityThreshold = 0.0;
|
||||
state.settings.densityThreshold = 1000000000.0;
|
||||
state.settings.maxPerStep = 1;
|
||||
checked(foam.restore(state));
|
||||
local pool = eve.VolumeFluidDiffuse();
|
||||
if (checked(foam.advanceFromActor(source, pool, 0.02, 9)) != 1)
|
||||
throw "Foam actor source count mismatch";
|
||||
local result = checked(pool.snapshot()).particles[0];
|
||||
if (result.position[0] < 0.4)
|
||||
throw "Foam escaped its owning emitter actor";
|
||||
checked(source.step(1.0 / 120.0, 1));
|
||||
local interpolated = eve.VolumeFluidFoam();
|
||||
checked(interpolated.restore(state));
|
||||
local interpolatedPool = eve.VolumeFluidDiffuse();
|
||||
if (checked(interpolated.advanceFromActorInterpolated(source, interpolatedPool, 0.02, 9, 0.0)) != 1)
|
||||
throw "Interpolated foam source failed";
|
||||
local oldEndpoint = checked(interpolatedPool.snapshot()).particles[0].position[0];
|
||||
if (fabs(oldEndpoint - 0.55) > 0.00001)
|
||||
throw "Foam did not use the previous fixed-step position";
|
||||
if (interpolated.advanceFromActorInterpolated(source, interpolatedPool, 0.02, 9, 1.01).ok)
|
||||
throw "Invalid foam interpolation accepted";
|
||||
if (foam.advanceFromActor(source, pool, 0.02, -1).ok)
|
||||
throw "Negative actor group accepted";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Opt-in automatic foam generation/replay probe. Does not run a render loop.
|
||||
function verifyVolumeFluidFoam() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for (local i=0; i<2; ++i) {
|
||||
local p = clone prototype;
|
||||
p.position = [i==0 ? -0.05 : 0.05, 1.0, 0.0];
|
||||
p.velocity = [0.0, i==0 ? -1.0 : 1.0, 0.0];
|
||||
definition.particles.append(p);
|
||||
}
|
||||
local sim = checked(fluids.newVolumeSimulator(definition));
|
||||
local foam = eve.VolumeFluidFoam();
|
||||
local state = checked(foam.snapshot());
|
||||
state.settings.rate = 100.0;
|
||||
state.settings.vorticityThreshold = 0.0;
|
||||
state.settings.densityThreshold = 1000000.0;
|
||||
state.settings.maxPerStep = 1;
|
||||
checked(foam.restore(state));
|
||||
local pool = eve.VolumeFluidDiffuse();
|
||||
if (checked(foam.advance(sim, pool, 0.02))!=1) throw "Foam source selection failed";
|
||||
local generated = checked(pool.snapshot()).particles[0];
|
||||
local replay = eve.VolumeFluidFoam();
|
||||
local replayPool = eve.VolumeFluidDiffuse();
|
||||
checked(replay.restore(state));
|
||||
checked(replay.advance(sim, replayPool, 0.02));
|
||||
local repeated = checked(replayPool.snapshot()).particles[0];
|
||||
for(local i=0; i<3; ++i)
|
||||
if (generated.position[i]!=repeated.position[i]) throw "Foam replay mismatch";
|
||||
checked(pool.advance(sim, 0.01, 1));
|
||||
if (checked(pool.snapshot()).particles[0].life>=generated.life) throw "Foam did not advect/age";
|
||||
local agedLife = checked(pool.snapshot()).particles[0].life;
|
||||
for (local i=0; i<201; ++i) checked(pool.advance(sim, 0.01, 0));
|
||||
if (checked(pool.snapshot()).particles.len()!=0) throw "Expired foam was not returned to the pool";
|
||||
state.settings.densityThreshold = 0.0;
|
||||
checked(foam.restore(state));
|
||||
if (checked(foam.advance(sim, pool, 0.02))!=0) throw "Density filter ignored";
|
||||
state.randomState = 0;
|
||||
if (foam.restore(state).ok) throw "Invalid foam RNG accepted";
|
||||
if (foam.advance(sim, pool, -0.01).ok) throw "Invalid foam dt accepted";
|
||||
return "VOLUME_FLUID_FOAM_PASS emitted=1 agedLife=" + agedLife + " expired=1";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Opt-in check for Fluid3D generateFoam and foamDownsample renderer settings.
|
||||
function verifyFoamRenderSettings() {
|
||||
local localSurface = fluids.newSurfaceRenderer(64, 64);
|
||||
local configured = localSurface.configureFoam(false, 4);
|
||||
if (!configured.ok) throw configured.error.message;
|
||||
configured = localSurface.configureFoam(true, 2);
|
||||
if (!configured.ok) throw configured.error.message;
|
||||
if (localSurface.configureFoam(true, 0).ok)
|
||||
throw "Invalid foam downsample was accepted";
|
||||
return "VOLUME_FOAM_RENDER_SETTINGS_PASS";
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: DejaVu fonts
|
||||
Upstream-Author: Stepan Roh <src@users.sourceforge.net> (original author),
|
||||
see /usr/share/doc/fonts-dejavu-core/AUTHORS for full list
|
||||
Source: https://dejavu-fonts.github.io/
|
||||
|
||||
Files: *
|
||||
Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
|
||||
Bitstream Vera is a trademark of Bitstream, Inc.
|
||||
DejaVu changes are in public domain.
|
||||
License: bitstream-vera
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of the fonts accompanying this license ("Fonts") and associated
|
||||
documentation files (the "Font Software"), to reproduce and distribute the
|
||||
Font Software, including without limitation the rights to use, copy, merge,
|
||||
publish, distribute, and/or sell copies of the Font Software, and to permit
|
||||
persons to whom the Font Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
.
|
||||
The above copyright and trademark notices and this permission notice shall
|
||||
be included in all copies of one or more of the Font Software typefaces.
|
||||
.
|
||||
The Font Software may be modified, altered, or added to, and in particular
|
||||
the designs of glyphs or characters in the Fonts may be modified and
|
||||
additional glyphs or characters may be added to the Fonts, only if the fonts
|
||||
are renamed to names not containing either the words "Bitstream" or the word
|
||||
"Vera".
|
||||
.
|
||||
This License becomes null and void to the extent applicable to Fonts or Font
|
||||
Software that has been modified and is distributed under the "Bitstream
|
||||
Vera" names.
|
||||
.
|
||||
The Font Software may be sold as part of a larger software package but no
|
||||
copy of one or more of the Font Software typefaces may be sold by itself.
|
||||
.
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
||||
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
||||
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
|
||||
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
|
||||
FONT SOFTWARE.
|
||||
.
|
||||
Except as contained in this notice, the names of Gnome, the Gnome
|
||||
Foundation, and Bitstream Inc., shall not be used in advertising or
|
||||
otherwise to promote the sale, use or other dealings in this Font Software
|
||||
without prior written authorization from the Gnome Foundation or Bitstream
|
||||
Inc., respectively. For further information, contact: fonts at gnome dot
|
||||
org.
|
||||
|
||||
Files: debian/*
|
||||
Copyright: (C) 2005-2006 Peter Cernak <pce@users.sourceforge.net>
|
||||
(C) 2006-2011 Davide Viti <zinosat@tiscali.it>
|
||||
(C) 2011-2013 Christian Perrier <bubulle@debian.org>
|
||||
(C) 2013 Fabian Greffrath <fabian+debian@greffrath.com>
|
||||
License: GPL-2+
|
||||
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 2 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 package; if not, write to the Free
|
||||
Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
Boston, MA 02110-1301 USA
|
||||
.
|
||||
On Debian systems, the full text of the GNU General Public
|
||||
License version 2 can be found in the file
|
||||
/usr/share/common-licenses/GPL-2'.
|
||||
@@ -0,0 +1,65 @@
|
||||
// Opt-in FluidGranular acceptance probe. Loading this file has no side effects.
|
||||
function granularPair(friction, rolling) {
|
||||
local definition=checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity=[0.0,0.0,0.0];
|
||||
definition.settings.iterations=1;
|
||||
local prototype=checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
local particles=[];
|
||||
for(local i=0;i<2;++i) {
|
||||
local particle=clone prototype;
|
||||
particle.material=clone prototype.material;
|
||||
particle.position=[i==0 ? -0.045 : 0.045,1.0,0.0];
|
||||
particle.velocity=[0.0,0.0,i==0 ? 1.0 : -1.0];
|
||||
particle.material.phase=2;
|
||||
particle.material.cohesion=0.0;
|
||||
particle.material.staticFriction=friction;
|
||||
particle.material.dynamicFriction=friction;
|
||||
particle.material.rollingContacts=rolling;
|
||||
particle.material.rollingFriction=0.0;
|
||||
particles.append(particle);
|
||||
}
|
||||
definition.particles=particles;
|
||||
local sim=checked(fluids.newVolumeSimulator(definition));
|
||||
checked(sim.step(1.0/120.0,1));
|
||||
return checked(sim.snapshot()).particles;
|
||||
}
|
||||
|
||||
function verifyVolumeFluidGranular() {
|
||||
local definition=checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity=[0.0,0.0,0.0];
|
||||
definition.settings.capacity=128;
|
||||
local sim=checked(fluids.newVolumeSimulator(definition));
|
||||
local emission=checked(fluids.volumeEmissionDefaults());
|
||||
emission.description.origin=[0.0,1.0,0.0];
|
||||
emission.description.extent=[0.0,0.0,0.0];
|
||||
emission.description.seed=91;
|
||||
emission.description.prototype.material.phase=2;
|
||||
emission.description.granularRadiusRandomness=100.0;
|
||||
checked(sim.emitBurst(emission,64));
|
||||
local particles=checked(sim.snapshot()).particles;
|
||||
local minimumRadius=1000.0,maximumRadius=0.0,varied=0;
|
||||
foreach(particle in particles) {
|
||||
local radius=particle.radii[0];
|
||||
if(radius<0.001||radius>0.051001||radius!=particle.radii[1]||radius!=particle.radii[2])
|
||||
throw "Granular radius outside deterministic isotropic bounds";
|
||||
if(radius<minimumRadius)minimumRadius=radius;
|
||||
if(radius>maximumRadius)maximumRadius=radius;
|
||||
if(radius<0.049)++varied;
|
||||
}
|
||||
if(varied<32||maximumRadius-minimumRadius<0.02)
|
||||
throw "Granular radius randomness produced insufficient variation";
|
||||
local before=sim.getParticleCount();
|
||||
emission.description.granularRadiusRandomness=100.01;
|
||||
if(sim.emitBurst(emission,1).ok||sim.getParticleCount()!=before)
|
||||
throw "Invalid granular randomness was not atomic";
|
||||
|
||||
local free=granularPair(0.0,false),rough=granularPair(1.0,true);
|
||||
local freeRelative=fabs(free[0].velocity[2]-free[1].velocity[2]);
|
||||
local roughRelative=fabs(rough[0].velocity[2]-rough[1].velocity[2]);
|
||||
if(roughRelative>=freeRelative-0.1)
|
||||
throw "Granular static/dynamic friction did not reduce sliding";
|
||||
if(fabs(rough[0].angularVelocity[1])<0.1||fabs(rough[1].angularVelocity[1])<0.1)
|
||||
throw "Granular rolling contact did not generate spin";
|
||||
return "VOLUME_FLUID_GRANULAR_PASS varied="+varied+" minRadius="+minimumRadius+
|
||||
" maxRadius="+maximumRadius+" freeSlip="+freeRelative+" roughSlip="+roughRelative;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.particles = [];
|
||||
local solver = checked(fluids.newVolumeSimulator(definition));
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
local a = clone prototype;
|
||||
local b = clone prototype;
|
||||
local c = clone prototype;
|
||||
a.position = [-1.90, 0.10, -0.90];
|
||||
b.position = [-1.85, 0.15, -0.85];
|
||||
c.position = [-1.50, 0.10, -0.90];
|
||||
checked(solver.emit([a, b, c]));
|
||||
local cells = checked(solver.debugParticleGrid(2));
|
||||
if (cells.len() != 2) throw "particle grid occupied-cell count mismatch";
|
||||
if (cells[0].particleCount != 2 || cells[1].particleCount != 1)
|
||||
throw "particle grid population mismatch";
|
||||
if (solver.debugParticleGrid(1).ok) throw "particle grid budget accepted partial output";
|
||||
::gridDebuggerPass <- true;
|
||||
@@ -0,0 +1,16 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
definition.particles = [];
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
local terrain = checked(fluids.volumeHeightFieldColliderDefaults());
|
||||
terrain.label = 71;
|
||||
terrain.resolution = [2, 2];
|
||||
terrain.size = [1.0, 1.0, 1.0];
|
||||
terrain.heights = [0.0, 0.5, 0.0, 0.5];
|
||||
checked(localSolver.setHeightFieldColliders([terrain]));
|
||||
local particle = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.5, 0.1, 0.5];
|
||||
checked(localSolver.emit([particle]));
|
||||
checked(localSolver.step(1.0 / 120.0, 1));
|
||||
if (checked(localSolver.contacts()).len() != 1) throw "height-field contact missing";
|
||||
::heightFieldPass <- true;
|
||||
@@ -0,0 +1,22 @@
|
||||
// Opt-in FluidKarmanVortex acceptance. Loading this file is inert.
|
||||
function verifyVolumeFluidKarmanVortex() {
|
||||
paused=true;rebuild(18);
|
||||
local positive=0;local negative=0;local peak=0.0;local peakContacts=0;
|
||||
for(local frame=0;frame<180;++frame) {
|
||||
checked(solver.step(1.0/120.0,1));
|
||||
if(frame%12==11) {
|
||||
local wake=fluidKarmanSampleWake(karman,solver);
|
||||
positive+=wake.positive;negative+=wake.negative;if(wake.peak>peak)peak=wake.peak;
|
||||
local contacts=checked(solver.contacts()).len();if(contacts>peakContacts)peakContacts=contacts;
|
||||
}
|
||||
}
|
||||
local upstream=checked(solver.sampleField([[-0.95,0.765,0.0]]))[0];
|
||||
if(peakContacts<=0 || positive<=0 || negative<=0 || peak<0.1 ||
|
||||
peak<=fabs(upstream.vorticity[2]))
|
||||
throw "Karman wake validation failed contacts="+peakContacts+" positive="+positive+
|
||||
" negative="+negative+" peak="+peak+" upstream="+fabs(upstream.vorticity[2]);
|
||||
karman.peakContacts=peakContacts;karman.peakWakeCurl=peak;dirty=true;
|
||||
return "VOLUME_FLUID_KARMAN_PASS particles="+solver.getParticleCount()+" contacts="+
|
||||
peakContacts+" positive="+positive+" negative="+negative+" peakCurl="+format("%.3f",peak)+
|
||||
" upstreamCurl="+format("%.3f",fabs(upstream.vorticity[2]));
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Bounded Karman wake helper. Loading this file is inert.
|
||||
function fluidKarmanScene() {
|
||||
local definition=checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity=192;definition.settings.spacing=0.085;
|
||||
definition.settings.gravity=[0.0,0.0,0.0];
|
||||
definition.settings.minimum=[-1.3,0.15,-0.14];definition.settings.maximum=[1.3,1.45,0.14];
|
||||
definition.settings.iterations=2;
|
||||
local prototype=checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for(local y=0;y<10;++y) for(local x=0;x<18;++x) {
|
||||
local p=clone prototype;p.material=clone prototype.material;
|
||||
p.material.viscosity=0.08;p.material.cohesion=0.0;p.material.vorticity=0.2;
|
||||
p.position=[-1.12+x*0.09,0.36+y*0.09,((x+y)%2 ? -0.018 : 0.018)];
|
||||
p.velocity=[1.05,0.025*sin(y*2.3+x*0.7),0.0];
|
||||
p.color=[0.04,0.42+0.025*y,0.95,0.92];
|
||||
definition.particles.append(p);
|
||||
}
|
||||
local obstacle=checked(fluids.volumeColliderDefaults());
|
||||
obstacle.label=401;obstacle.shape=0;obstacle.center=[-0.22,0.765,0.0];
|
||||
obstacle.radius=0.19;obstacle.friction=0.02;
|
||||
definition.colliders=[obstacle];
|
||||
return {solver=checked(fluids.newVolumeSimulator(definition)),obstacle=obstacle,
|
||||
peakContacts=0,peakWakeCurl=0.0};
|
||||
}
|
||||
|
||||
function fluidKarmanSampleWake(scene,fluid) {
|
||||
local points=[];
|
||||
foreach(x in [0.08,0.28,0.48,0.68]) foreach(y in [0.50,0.63,0.765,0.90,1.03])
|
||||
points.append([x,y,0.0]);
|
||||
local samples=checked(fluid.sampleField(points));
|
||||
local positive=0;local negative=0;local peak=0.0;
|
||||
foreach(sample in samples) {
|
||||
local curl=sample.vorticity[2];if(curl>0.1)++positive;if(curl < -0.1)++negative;
|
||||
if(fabs(curl)>peak)peak=fabs(curl);
|
||||
}
|
||||
if(peak>scene.peakWakeCurl)scene.peakWakeCurl=peak;
|
||||
return {positive=positive,negative=negative,peak=peak};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Opt-in owning particle-pool lifecycle check.
|
||||
function verifyVolumeFluidLifecycle() {
|
||||
local definition=checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity=[0.0,0.0,0.0];
|
||||
local prototype=checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
local particles=[];
|
||||
for(local i=0;i<3;++i) {
|
||||
local particle=clone prototype;
|
||||
particle.position=[-0.2+i*0.2,1.0,0.0];
|
||||
particles.append(particle);
|
||||
}
|
||||
local testSolver=checked(fluids.newVolumeSimulator(definition));
|
||||
checked(testSolver.emit(particles));
|
||||
checked(testSolver.killParticle(1));
|
||||
local state=checked(testSolver.snapshot());
|
||||
if(testSolver.getParticleCount()!=2||state.particles.len()!=2||
|
||||
fabs(state.particles[0].position[0]+0.2)>0.00001||
|
||||
fabs(state.particles[1].position[0]-0.2)>0.00001)
|
||||
throw "Particle kill did not compact the owning pool";
|
||||
local invalid=testSolver.killParticle(2);
|
||||
if(invalid.ok||testSolver.getParticleCount()!=2)
|
||||
throw "Stale particle kill was not atomic";
|
||||
testSolver.clear();
|
||||
if(testSolver.getParticleCount()!=0)throw "Kill-all clear failed";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
persist solver = null;
|
||||
persist surface = null;
|
||||
persist pixels = null;
|
||||
persist imageModule = null;
|
||||
persist texture = null;
|
||||
persist accumulator = 0.0;
|
||||
persist paused = true;
|
||||
persist dirty = true;
|
||||
persist displayAccumulator = 0.0;
|
||||
persist droppedTime = 0.0;
|
||||
persist failure = "";
|
||||
persist keys = {};
|
||||
persist mode = 0;
|
||||
persist labelFont = null;
|
||||
persist emitter = null;
|
||||
persist nozzle = null;
|
||||
persist simulationTime = 0.0;
|
||||
persist wheelWorld = null;
|
||||
persist wheelBody = null;
|
||||
persist wheelCoupling = null;
|
||||
persist wheelTexture = null;
|
||||
persist wheelContacts = 0;
|
||||
persist simulationMs = 0.0;
|
||||
persist reconstructionMs = 0.0;
|
||||
persist uploadMs = 0.0;
|
||||
persist slowWorkCount = 0;
|
||||
persist thermalRules = [];
|
||||
persist viscosityColors = [];
|
||||
persist attachmentCollider = null;
|
||||
persist foamGenerator = null;
|
||||
persist foamPool = null;
|
||||
persist foamCompositeMs = 0.0;
|
||||
persist sdfPose = null;
|
||||
persist overlapQueries = [];
|
||||
persist overlapCounts = [];
|
||||
persist maze = null;
|
||||
persist bucket = null;
|
||||
persist bottle = null;
|
||||
persist karman = null;
|
||||
persist refractionBackground = null;
|
||||
persist obstacleTexture = null;
|
||||
|
||||
dofile("maze-support.nut");
|
||||
dofile("bucket-support.nut");
|
||||
dofile("bottle-support.nut");
|
||||
dofile("karman-support.nut");
|
||||
dofile("wave-support.nut");
|
||||
|
||||
function updateOverlapTriggers(t) {
|
||||
overlapQueries[0].center = [-0.38 + 0.24 * sin(t), 0.72, 0.0];
|
||||
overlapQueries[1].center = [0.38 + 0.24 * cos(t * 0.8), 0.72, 0.0];
|
||||
overlapCounts = checked(solver.applyQueryColors(overlapQueries,
|
||||
[[1.0, 0.08, 0.04, 1.0], [1.0, 0.85, 0.04, 1.0]],
|
||||
0.04, 0.75, 0.9, 1.0, 1024));
|
||||
}
|
||||
|
||||
function advanceAttachment(t) {
|
||||
attachmentCollider.center = fluidWavePosition([0.0, 0.5, 0.0], 0.3, 1.0, t);
|
||||
local angle = 0.35 * sin(t);
|
||||
attachmentCollider.rotation = [0.0, 0.0, sin(angle * 0.5), cos(angle * 0.5)];
|
||||
checked(solver.setColliders([attachmentCollider]));
|
||||
checked(solver.step(1.0 / 60.0, 2));
|
||||
checked(solver.applySolidColor(0.95, 0.65, 0.2, 1.0));
|
||||
}
|
||||
|
||||
// Stop sustained overload before submitting more fluid work. This cannot
|
||||
// interrupt an in-flight GPU command or recover a lost device.
|
||||
function checkWorkBudget(milliseconds) {
|
||||
slowWorkCount = milliseconds > 50.0 ? slowWorkCount + 1 : 0;
|
||||
if (milliseconds > 250.0 || slowWorkCount >= 3)
|
||||
throw "Fluid work budget exceeded: " + format("%.1f ms", milliseconds);
|
||||
}
|
||||
|
||||
function movingPose(t) {
|
||||
local halfAngle = 0.25 * sin(2.0 * t);
|
||||
local c = 0.70710678118 * cos(halfAngle);
|
||||
local s = 0.70710678118 * sin(halfAngle);
|
||||
return { position = [0.35 * sin(t), 1.7, 0.0], rotation = [c, s, s, c] };
|
||||
}
|
||||
|
||||
function drawMillStructure(angle, foreground) {
|
||||
local wood = foreground ? 0.72 : 0.30;
|
||||
local green = foreground ? 0.43 : 0.20;
|
||||
local blue = foreground ? 0.16 : 0.10;
|
||||
if (!foreground) {
|
||||
// Static frame and tail race. These are presentation geometry only;
|
||||
// the fluid solver still sees the same two bounded paddle colliders.
|
||||
gfx.drawTexturedRectRotated(wheelTexture,318.0,496.0,28.0,274.0,-18.0,0.24,0.20,0.16,1.0);
|
||||
gfx.drawTexturedRectRotated(wheelTexture,642.0,496.0,28.0,274.0,18.0,0.24,0.20,0.16,1.0);
|
||||
gfx.drawTexturedRectRotated(wheelTexture,480.0,565.0,390.0,22.0,0.0,0.20,0.25,0.28,1.0);
|
||||
gfx.drawTexturedRectRotated(wheelTexture,650.0,248.0,250.0,14.0,-6.0,0.16,0.19,0.21,1.0);
|
||||
}
|
||||
// A segmented rim gives the original mill silhouette without adding rigid
|
||||
// bodies or fluid colliders. Four visible paddles match the two crossbars.
|
||||
for(local i=0;i<24;++i) {
|
||||
local a=angle+i*15.0;
|
||||
local r=a*3.14159265359/180.0;
|
||||
gfx.drawTexturedRectRotated(wheelTexture,480.0+cos(r)*82.0,360.0-sin(r)*82.0,
|
||||
24.0,10.0,-a+90.0,wood,green,blue,foreground ? 0.96 : 0.55);
|
||||
}
|
||||
if(foreground) {
|
||||
gfx.drawTexturedRectRotated(wheelTexture,480.0,360.0,161.7,18.0,angle,0.68,0.40,0.14,0.92);
|
||||
gfx.drawTexturedRectRotated(wheelTexture,480.0,360.0,161.7,18.0,angle+90.0,0.68,0.40,0.14,0.92);
|
||||
gfx.drawTexturedRect(wheelTexture,468.0,348.0,24.0,24.0,0.28,0.18,0.10,1.0);
|
||||
}
|
||||
}
|
||||
|
||||
function checked(result) {
|
||||
if (!result.ok) throw result.status.summary;
|
||||
return result.value;
|
||||
}
|
||||
|
||||
function advanceWheelJet(dt) {
|
||||
return checked(emitter.advance(solver,nozzle,dt,90.0,4,0.2));
|
||||
}
|
||||
|
||||
function mazeStateValid() {
|
||||
return maze != null && "walls" in maze && typeof maze.walls == "array" &&
|
||||
maze.walls.len() > 0 && "center" in maze.walls[0] &&
|
||||
typeof maze.walls[0].center == "array" && maze.walls[0].center.len() >= 2 &&
|
||||
"half" in maze.walls[0] && typeof maze.walls[0].half == "array" &&
|
||||
maze.walls[0].half.len() >= 2;
|
||||
}
|
||||
|
||||
function bucketStateValid() {
|
||||
return bucket != null && "walls" in bucket && typeof bucket.walls == "array" &&
|
||||
bucket.walls.len() > 0 && "center" in bucket.walls[0] &&
|
||||
typeof bucket.walls[0].center == "array" && bucket.walls[0].center.len() >= 2 &&
|
||||
"half" in bucket.walls[0] && typeof bucket.walls[0].half == "array" &&
|
||||
bucket.walls[0].half.len() >= 2;
|
||||
}
|
||||
|
||||
function bottleStateValid() {
|
||||
return bottle != null && "pose" in bottle && typeof bottle.pose == "table" &&
|
||||
"position" in bottle.pose && typeof bottle.pose.position == "array" && bottle.pose.position.len() == 3;
|
||||
}
|
||||
|
||||
function pressed(key) {
|
||||
local now = keyboard.isDown(key);
|
||||
local previous = key in keys ? keys[key] : false;
|
||||
keys[key] <- now;
|
||||
return now && !previous;
|
||||
}
|
||||
|
||||
function rebuild(selected) {
|
||||
wheelCoupling = null;
|
||||
wheelBody = null;
|
||||
if(wheelWorld != null) {wheelWorld.destroy();wheelWorld=null;}
|
||||
wheelContacts = 0;
|
||||
mode = selected;
|
||||
thermalRules = [];
|
||||
viscosityColors = [];
|
||||
attachmentCollider = null;
|
||||
foamGenerator = null;
|
||||
foamPool = null;
|
||||
foamCompositeMs = 0.0;
|
||||
sdfPose = null;
|
||||
overlapQueries = [];
|
||||
overlapCounts = [];
|
||||
maze = null;
|
||||
bucket = null;
|
||||
bottle = null;
|
||||
karman = null;
|
||||
if(surface != null) {
|
||||
checked(surface.configureSurface(1.0,0.0,0.03,2));
|
||||
checked(surface.configureSurfaceBlurRadius(0.02));
|
||||
checked(surface.configureSurfaceDownsample(1));
|
||||
checked(surface.configureThicknessDownsample(1));
|
||||
checked(surface.configureAnisotropy(mode == 3));
|
||||
if(mode == 3) checked(surface.configureSurfaceDownsample(2));
|
||||
checked(surface.configureMaterial(true,0.8,0.0,0.55,0.75,0.35));
|
||||
checked(surface.configureReflection(true));
|
||||
checked(surface.configureFoam(true,1));
|
||||
checked(surface.configureColors(0.05,0.32,0.72,0.55,0.72,1.0));
|
||||
checked(surface.configureRefraction(1.0,5.0,0.01,1));
|
||||
checked(surface.configureRefractionEnabled(true));
|
||||
if(mode == 5) surface.setCameraXYZ(0.0, 4.0, 0.1, 0.0, 1.3, 0.0, 48.0);
|
||||
else surface.setCameraXYZ(3.4, 3.0, 5.0, 0.0, 0.9, 0.0, 48.0);
|
||||
}
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 2048;
|
||||
definition.settings.spacing = 0.12;
|
||||
definition.settings.minimum = [-1.2, 0.0, -0.75];
|
||||
definition.settings.maximum = [1.2, 2.4, 0.75];
|
||||
// Interactive default: two stable 1/120 s substeps and two PBF iterations.
|
||||
// The lab runs the CPU reference solver, so extra quality passes are opt-in
|
||||
// per scenario instead of consuming the presentation budget in every mode.
|
||||
definition.settings.iterations = 2;
|
||||
local defaultParticle = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for (local z = 0; z < (mode >= 4 && mode != 14 ? 0 : 8); ++z)
|
||||
for (local y = 0; y < 10; ++y)
|
||||
for (local x = 0; x < 8; ++x) {
|
||||
local particle = clone defaultParticle;
|
||||
local material = clone defaultParticle.material;
|
||||
if (mode == 1) { material.viscosity = 70.0; material.yieldStress = 5.0; }
|
||||
if (mode == 2) { material.phase = 1; material.buoyancy = -0.4; material.drag = 1.0; }
|
||||
if (mode == 3) { material.phase = 2; material.cohesion = 0.0; }
|
||||
particle.position = [mode == 14 ? -0.42 + x * 0.12 : -1.0 + x * 0.12,
|
||||
0.12 + y * 0.12, -0.42 + z * 0.12];
|
||||
if (mode == 2) particle.color = [0.55, 0.58, 0.62, 0.35];
|
||||
particle.material = material;
|
||||
definition.particles.append(particle);
|
||||
}
|
||||
if(mode == 7) {
|
||||
definition.settings.spacing = 0.1;
|
||||
definition.settings.minimum = [-0.3,0.0,-0.3];
|
||||
definition.settings.maximum = [0.3,1.4,0.3];
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for(local z=0;z<5;++z) for(local y=0;y<10;++y) for(local x=0;x<5;++x) {
|
||||
local p=clone prototype;
|
||||
p.material=clone prototype.material;
|
||||
p.material.density=y<5 ? 700.0 : 1300.0;
|
||||
p.material.cohesion=0.0;p.material.viscosity=0.05;
|
||||
p.position=[-0.2+x*0.1+0.015*sin(y+z),0.071+y*0.1+0.015*sin(x+z),-0.2+z*0.1+0.015*cos(x+y)];
|
||||
p.color=y<5 ? [0.1,0.5,1.0,1.0] : [1.0,0.2,0.08,1.0];
|
||||
definition.particles.append(p);
|
||||
}
|
||||
surface.setCameraXYZ(1.5,1.6,2.4,0.0,0.5,0.0,48.0);
|
||||
}
|
||||
if (mode == 9) {
|
||||
definition.settings.spacing = 0.1;
|
||||
definition.settings.minimum = [-0.6, 0.0, -0.3];
|
||||
definition.settings.maximum = [0.6, 1.2, 0.3];
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for (local z = 0; z < 5; ++z) for (local y = 0; y < 5; ++y) for (local x = 0; x < 10; ++x) {
|
||||
local p = clone prototype;
|
||||
p.material = clone prototype.material;
|
||||
p.material.diffusion = 20.0;
|
||||
p.material.cohesion = x < 5 ? 0.5 : 2.0;
|
||||
p.material.viscosity = x < 5 ? 2.0 : 50.0;
|
||||
p.data = [p.material.viscosity, p.material.cohesion, 0.0, 0.0];
|
||||
p.position = [-0.45 + x * 0.1, 0.1 + y * 0.1, -0.2 + z * 0.1];
|
||||
p.velocity = [x < 5 ? 0.4 : -0.4, 0.0, 0.0];
|
||||
p.color = x < 5 ? [1.0, 0.1, 0.05, 1.0] : [0.05, 0.2, 1.0, 1.0];
|
||||
definition.particles.append(p);
|
||||
}
|
||||
surface.setCameraXYZ(0.0, 1.4, 2.1, 0.0, 0.25, 0.0, 48.0);
|
||||
}
|
||||
solver = checked(fluids.newVolumeSimulator(definition));
|
||||
emitter = eve.VolumeFluidJetEmitter();
|
||||
if (mode == 14) {
|
||||
overlapQueries = [
|
||||
{type=1,center=[-0.38,0.72,0.0],size=[0.65,0.85,1.1],rotation=[0.0,0.0,0.0,1.0],contactOffset=0.0,maxDistance=0.0,phaseMask=1,collisionFilter=4294901761},
|
||||
{type=1,center=[0.38,0.72,0.0],size=[0.65,0.85,1.1],rotation=[0.0,0.0,0.258819,0.965926],contactOffset=0.0,maxDistance=0.0,phaseMask=1,collisionFilter=4294901761}
|
||||
];
|
||||
updateOverlapTriggers(0.0);
|
||||
surface.setCameraXYZ(2.7,2.1,3.8,0.0,0.65,0.0,46.0);
|
||||
}
|
||||
if (mode == 10) {
|
||||
local thermal = checked(fluids.volumeDefaults());
|
||||
thermal.settings.capacity = 128;
|
||||
thermal.settings.spacing = 0.1;
|
||||
thermal.settings.minimum = [-1.0, 0.0, -0.4];
|
||||
thermal.settings.maximum = [1.0, 1.2, 0.4];
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for (local side = 0; side < 2; ++side)
|
||||
for (local z = 0; z < 4; ++z) for (local y = 0; y < 3; ++y) for (local x = 0; x < 4; ++x) {
|
||||
local p = clone prototype;
|
||||
p.material = clone prototype.material;
|
||||
p.material.viscosity = 5.0; p.material.cohesion = 1.0; p.material.diffusion = 8.0;
|
||||
p.position = [(side == 0 ? -0.45 : 0.45) - 0.15 + x * 0.1, 0.34 + y * 0.1, -0.15 + z * 0.1];
|
||||
p.data = [5.0, 1.0, 0.0, 0.0];
|
||||
thermal.particles.append(p);
|
||||
}
|
||||
solver = checked(fluids.newVolumeSimulator(thermal));
|
||||
local colliders = [];
|
||||
for (local side = 0; side < 2; ++side) {
|
||||
local plate = checked(fluids.volumeColliderDefaults());
|
||||
plate.label = side + 1; plate.shape = 1;
|
||||
plate.center = [side == 0 ? -0.45 : 0.45, 0.25, 0.0];
|
||||
plate.halfExtent = [0.35, 0.05, 0.3];
|
||||
colliders.append(plate);
|
||||
local rule = checked(fluids.volumeThermalRuleDefaults());
|
||||
rule.colliderLabel = plate.label; rule.rate = side == 0 ? -2.0 : 2.0;
|
||||
thermalRules.append(rule);
|
||||
}
|
||||
checked(solver.setColliders(colliders));
|
||||
viscosityColors = [
|
||||
{viscosity = 0.05, color = [1.0, 0.12, 0.02, 1.0]},
|
||||
{viscosity = 5.0, color = [1.0, 0.85, 0.15, 1.0]},
|
||||
{viscosity = 10.0, color = [0.08, 0.35, 1.0, 1.0]}
|
||||
];
|
||||
checked(solver.applyMaterialChannelsWithColors(viscosityColors));
|
||||
surface.setCameraXYZ(0.0, 1.4, 3.0, 0.0, 0.35, 0.0, 48.0);
|
||||
}
|
||||
if (mode == 11) {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 32;
|
||||
definition.settings.spacing = 0.1;
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for (local z = 0; z < 4; ++z) for (local x = 0; x < 4; ++x) {
|
||||
local p = clone prototype;
|
||||
p.position = [-0.15 + x * 0.1, 0.59, -0.15 + z * 0.1];
|
||||
definition.particles.append(p);
|
||||
}
|
||||
solver = checked(fluids.newVolumeSimulator(definition));
|
||||
attachmentCollider = checked(fluids.volumeColliderDefaults());
|
||||
attachmentCollider.label = 31; attachmentCollider.shape = 1;
|
||||
attachmentCollider.center = [0.0, 0.5, 0.0];
|
||||
attachmentCollider.halfExtent = [0.4, 0.05, 0.4];
|
||||
attachmentCollider.solidify = true;
|
||||
checked(solver.setColliders([attachmentCollider]));
|
||||
surface.setCameraXYZ(1.0, 1.4, 2.4, 0.0, 0.55, 0.0, 48.0);
|
||||
}
|
||||
if (mode == 12) {
|
||||
checked(surface.configureFoam(true,2));
|
||||
local foamDefinition = checked(fluids.volumeDefaults());
|
||||
foamDefinition.settings.capacity = 256;
|
||||
foamDefinition.settings.spacing = 0.1;
|
||||
foamDefinition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for (local z=0; z<8; ++z) for (local y=0; y<3; ++y) for (local x=0; x<8; ++x) {
|
||||
local p = clone prototype;
|
||||
p.material = clone prototype.material;
|
||||
p.material.viscosity = 0.05;
|
||||
p.position = [-0.35+x*0.1, 0.7+y*0.1, -0.35+z*0.1];
|
||||
p.velocity = [-2.0*p.position[2], 0.0, 2.0*p.position[0]];
|
||||
p.color = [0.05, 0.3, 0.7, 1.0];
|
||||
foamDefinition.particles.append(p);
|
||||
}
|
||||
solver = checked(fluids.newVolumeSimulator(foamDefinition));
|
||||
foamPool = eve.VolumeFluidDiffuse();
|
||||
local poolState = checked(foamPool.snapshot());
|
||||
poolState.capacity = 512;
|
||||
checked(foamPool.restore(poolState));
|
||||
foamGenerator = eve.VolumeFluidFoam();
|
||||
local foamState = checked(foamGenerator.snapshot());
|
||||
foamState.settings.rate = 400.0;
|
||||
foamState.settings.vorticityThreshold = 0.5;
|
||||
// Native field calibration: surface sites are about 520-700 kg/m3 in
|
||||
// this bounded vortex, while the interior reaches about 1010 kg/m3.
|
||||
foamState.settings.densityThreshold = 700.0;
|
||||
foamState.settings.randomness = 0.1;
|
||||
foamState.settings.maxPerStep = 8;
|
||||
checked(foamGenerator.restore(foamState));
|
||||
checked(foamGenerator.advance(solver, foamPool, 0.02));
|
||||
surface.setCameraXYZ(0.0, 2.0, 2.0, 0.0, 0.8, 0.0, 42.0);
|
||||
}
|
||||
if (mode == 13) {
|
||||
local sdfDefinition = checked(fluids.volumeDefaults());
|
||||
sdfDefinition.settings.capacity = 512;
|
||||
sdfDefinition.settings.spacing = 0.1;
|
||||
sdfDefinition.settings.minimum = [-1.0, 0.0, -0.65];
|
||||
sdfDefinition.settings.maximum = [1.0, 2.2, 0.65];
|
||||
sdfDefinition.settings.iterations = 3;
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for (local z = 0; z < 6; ++z)
|
||||
for (local y = 0; y < 6; ++y)
|
||||
for (local x = 0; x < 8; ++x) {
|
||||
local p = clone prototype;
|
||||
p.material = clone prototype.material;
|
||||
p.material.viscosity = 1.0;
|
||||
p.position = [-0.38 + x * 0.1, 1.25 + y * 0.1, -0.25 + z * 0.1];
|
||||
p.color = [0.04, 0.38, 0.9, 1.0];
|
||||
sdfDefinition.particles.append(p);
|
||||
}
|
||||
solver = checked(fluids.newVolumeSimulator(sdfDefinition));
|
||||
local obstacle = checked(fluids.volumeSdfColliderDefaults());
|
||||
obstacle.label = 51;
|
||||
obstacle.position = [0.0, 0.72, 0.0];
|
||||
obstacle.scale = 0.85;
|
||||
obstacle.friction = 0.05;
|
||||
checked(solver.setSdfColliders([obstacle]));
|
||||
sdfPose = checked(fluids.volumeSdfPoseDefaults());
|
||||
sdfPose.label = obstacle.label;
|
||||
sdfPose.position = obstacle.position;
|
||||
sdfPose.scale = obstacle.scale;
|
||||
surface.setCameraXYZ(2.6, 2.2, 3.8, 0.0, 0.85, 0.0, 46.0);
|
||||
}
|
||||
if (mode == 15) {
|
||||
maze = fluidMazeScene();
|
||||
solver = maze.solver;
|
||||
delete maze.solver;
|
||||
surface.setCameraXYZ(0.0,1.2,5.0,0.0,1.2,0.0,48.0);
|
||||
}
|
||||
if (mode == 16) {
|
||||
bucket=fluidBucketScene();
|
||||
solver=bucket.solver;delete bucket.solver;
|
||||
surface.setCameraXYZ(0.0,1.1,5.0,0.0,1.1,0.0,48.0);
|
||||
}
|
||||
if (mode == 17) {
|
||||
bottle=fluidBottleScene();
|
||||
solver=bottle.solver;delete bottle.solver;
|
||||
checked(surface.configureSurface(1.15,0.008,0.025,2));
|
||||
checked(surface.configureMaterial(true,0.92,0.05,0.62,0.32,0.55));
|
||||
checked(surface.configureColors(0.48,0.16,0.025,0.95,0.72,0.30));
|
||||
checked(surface.configureRefraction(0.9,3.5,0.012,2));
|
||||
surface.setCameraXYZ(0.0,1.35,4.2,0.0,0.95,0.0,43.0);
|
||||
}
|
||||
if(mode == 18) {
|
||||
karman=fluidKarmanScene();solver=karman.solver;delete karman.solver;
|
||||
surface.setCameraXYZ(0.0,1.65,3.4,0.0,0.78,0.0,42.0);
|
||||
}
|
||||
nozzle = checked(fluids.volumeEmissionDefaults());
|
||||
nozzle.description.origin = [-0.65, 1.6, 0.0];
|
||||
nozzle.description.direction = [1.0, 0.2, 0.0];
|
||||
nozzle.description.extent = [0.12, 0.12, 0.12];
|
||||
nozzle.description.speed = 1.5;
|
||||
nozzle.description.prototype.life = 1.5;
|
||||
if(mode == 16) {
|
||||
nozzle.description.origin=[0.0,2.05,0.0];nozzle.description.direction=[0.0,-1.0,0.0];
|
||||
nozzle.description.extent=[0.0,0.0,0.0];nozzle.description.speed=1.2;
|
||||
nozzle.description.prototype.life=1.8;
|
||||
}
|
||||
if(mode == 8) {
|
||||
// FluidMill is a sustained emitter scene. Cap active storage and lower
|
||||
// only the render targets; physics spacing and the shared SSF algorithm
|
||||
// stay unchanged.
|
||||
definition.settings.capacity=384;
|
||||
definition.settings.spacing=0.1;
|
||||
definition.settings.spacing=0.08;
|
||||
checked(surface.configureSurfaceDownsample(2));
|
||||
checked(surface.configureThicknessDownsample(4));
|
||||
surface.setCameraXYZ(0.0,1.0,4.0,0.0,1.0,0.0,48.0);
|
||||
wheelWorld=physics.newWorld3D(0.0,0.0,0.0,false);
|
||||
local anchor=wheelWorld.newBody("static",0.0,1.0,0.0);
|
||||
wheelBody=wheelWorld.newBody("dynamic",0.0,1.0,0.0);
|
||||
wheelBody.newBoxShape(0.8,0.1,0.2,1000.0,0.5,0.0);
|
||||
wheelBody.newBoxShape(0.1,0.8,0.2,1000.0,0.5,0.0);
|
||||
if(wheelWorld.newRevoluteJoint(anchor,wheelBody,0.0,1.0,0.0,0.0,0.0,1.0,false)==null)
|
||||
throw "Wheel hinge creation failed";
|
||||
wheelCoupling=eve.VolumeFluidCoupling();
|
||||
local paddle=checked(fluids.volumeColliderDefaults());
|
||||
paddle.shape=1;paddle.center=[0.0,0.0,0.0];paddle.halfExtent=[0.4,0.05,0.1];paddle.friction=0.5;
|
||||
paddle.label=1;checked(wheelCoupling.attach(wheelBody,paddle));
|
||||
paddle.label=2;paddle.rotation=[0.0,0.0,0.70710678118,0.70710678118];
|
||||
checked(wheelCoupling.attach(wheelBody,paddle));
|
||||
nozzle.description.origin=[0.25,2.0,0.0];nozzle.description.direction=[0.0,-1.0,0.0];
|
||||
nozzle.description.speed=2.0;nozzle.description.extent=[0.05,0.05,0.05];
|
||||
// The original FluidMill uses a sustained particle emitter. A bounded
|
||||
// rate controller forms a continuous sheet while capping each step.
|
||||
emitter=eve.VolumeFluidEmitter();
|
||||
}
|
||||
if(mode == 5) {
|
||||
local model = model3d.newModelDataFromFile("assets/emission-ring.obj");
|
||||
local shape = checked(fluids.volumeEmissionFromModel(model, 0, 1.0, 1.0, 1.0, definition.settings.spacing));
|
||||
shape.description.origin = [0.0, 1.3, 0.0];
|
||||
shape.description.direction = [0.0, 0.0, 1.0];
|
||||
shape.description.speed = 0.0;
|
||||
checked(solver.emitBurst(shape, shape.description.distribution.len()));
|
||||
}
|
||||
accumulator = 0.0;
|
||||
simulationTime = 0.0;
|
||||
dirty = true;
|
||||
droppedTime = 0.0;
|
||||
failure = "";
|
||||
simulationMs = 0.0;
|
||||
reconstructionMs = 0.0;
|
||||
uploadMs = 0.0;
|
||||
slowWorkCount = 0;
|
||||
paused = true;
|
||||
}
|
||||
|
||||
eve_init = function() {
|
||||
gfx.setBackgroundColor(0.025, 0.04, 0.065, 1.0);
|
||||
surface = fluids.newSurfaceRenderer(384, 288);
|
||||
// Build the existing fluids SSF backend before per-frame timing starts.
|
||||
checked(surface.prepare());
|
||||
surface.setCameraXYZ(3.4, 3.0, 5.0, 0.0, 0.9, 0.0, 48.0);
|
||||
imageModule = eve.Image();
|
||||
pixels = imageModule.newEmptyImageData(384, 288, "RGBA8");
|
||||
refractionBackground=imageModule.newEmptyImageData(384,288,"RGBA8");
|
||||
// Sparse high-contrast grid: fewer than 10k script/native calls at startup,
|
||||
// while still making normal-driven refraction displacement obvious.
|
||||
for(local x=0;x<384;x+=24) for(local y=0;y<288;++y)
|
||||
refractionBackground.setPixel(x,y,0.22,0.48,0.72,1.0);
|
||||
for(local y=0;y<288;y+=24) for(local x=0;x<384;++x)
|
||||
refractionBackground.setPixel(x,y,0.42,0.24,0.16,1.0);
|
||||
texture = gfx.newTexture(pixels, false, false);
|
||||
local white=imageModule.newEmptyImageData(1,1,"RGBA8");white.setPixel(0,0,1.0,1.0,1.0,1.0);
|
||||
wheelTexture=gfx.newTexture(white,false,false);
|
||||
local obstaclePixels=imageModule.newEmptyImageData(32,32,"RGBA8");
|
||||
for(local y=0;y<32;++y)for(local x=0;x<32;++x) {
|
||||
local dx=x-15.5;local dy=y-15.5;
|
||||
if(dx*dx+dy*dy<=240.25)obstaclePixels.setPixel(x,y,0.72,0.35,0.12,1.0);
|
||||
}
|
||||
obstacleTexture=gfx.newTexture(obstaclePixels,false,false);
|
||||
local fontData = eve.Font().newFontDataFromFile("fonts/DejaVuSans-Bold.ttf", 18);
|
||||
labelFont = gfx.newFont(fontData, " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789|:.-");
|
||||
gfx.setFont(labelFont);
|
||||
rebuild(0);
|
||||
};
|
||||
|
||||
eve_update = function(dt) {
|
||||
if (mode == 15 && !mazeStateValid()) rebuild(15);
|
||||
if (mode == 16 && !bucketStateValid()) rebuild(16);
|
||||
if (mode == 17 && !bottleStateValid()) rebuild(17);
|
||||
if (pressed("space")) paused = !paused;
|
||||
if (pressed("r")) rebuild(mode);
|
||||
for (local i = 0; i < 9; ++i) if (pressed((i + 1).tostring())) rebuild(i);
|
||||
if (pressed("0")) rebuild(9);
|
||||
if (pressed("t")) rebuild(10);
|
||||
if (pressed("s")) rebuild(11);
|
||||
if (pressed("f")) rebuild(12);
|
||||
if (mode != 16 && mode != 17 && pressed("d")) rebuild(13);
|
||||
if (pressed("q")) rebuild(14);
|
||||
if (pressed("m")) rebuild(15);
|
||||
if (pressed("b")) rebuild(16);
|
||||
if (pressed("w")) rebuild(17);
|
||||
if (pressed("k")) rebuild(18);
|
||||
// Bound work per presentation frame. Overload slows simulation time instead
|
||||
// of accumulating an unbounded catch-up queue; show discarded wall time.
|
||||
local presentationInterval = mode == 2 ? 1.0 / 15.0 : 1.0 / 30.0;
|
||||
displayAccumulator += dt;
|
||||
if (displayAccumulator > presentationInterval) displayAccumulator = presentationInterval;
|
||||
try {
|
||||
local frameWorkMs = 0.0;
|
||||
local simulatedThisFrame = false;
|
||||
// Alternate expensive CPU simulation and synchronous SSF readback.
|
||||
// `dirty` means the latest simulated state still needs presenting;
|
||||
// never let another simulation step starve that reconstruction.
|
||||
if (!paused && failure == "" && !dirty) {
|
||||
accumulator += dt;
|
||||
if (accumulator >= 1.0 / 60.0) {
|
||||
local simulationStart = clock();
|
||||
if(mode == 8) {
|
||||
wheelContacts+=checked(wheelCoupling.step(wheelWorld,solver,1.0/60.0,2));
|
||||
wheelWorld.update(1.0/60.0);
|
||||
advanceWheelJet(1.0/60.0);
|
||||
} else if (mode == 10) {
|
||||
checked(solver.stepWithThermalContacts(1.0 / 60.0, 4, thermalRules));
|
||||
checked(solver.applyMaterialChannelsWithColors(viscosityColors));
|
||||
} else if (mode == 11) advanceAttachment(simulationTime);
|
||||
else if (mode == 13) {
|
||||
sdfPose.position = [0.2 * sin(simulationTime), 0.72, 0.0];
|
||||
sdfPose.velocity = [0.2 * cos(simulationTime), 0.0, 0.0];
|
||||
checked(solver.updateSdfColliderPoses([sdfPose]));
|
||||
checked(solver.step(1.0 / 60.0, 2));
|
||||
}
|
||||
else if (mode == 15) {
|
||||
if (keyboard.isDown("a")) maze.angularSpeed += 5.0 / 60.0;
|
||||
if (keyboard.isDown("d")) maze.angularSpeed -= 5.0 / 60.0;
|
||||
maze.angularSpeed *= pow(0.8,1.0/60.0);
|
||||
if (fabs(maze.angularSpeed) < 0.0001) maze.angularSpeed = 0.0;
|
||||
maze.angle += maze.angularSpeed / 60.0;
|
||||
if (maze.angularSpeed != 0.0) {
|
||||
fluidMazeUpdateColliderPoses(maze.walls,maze.colliders,maze.angle);
|
||||
checked(solver.setColliders(maze.colliders));
|
||||
}
|
||||
checked(solver.step(1.0/60.0,2));
|
||||
fluidMazeApplyTriggers(maze,solver);
|
||||
}
|
||||
else if(mode == 16) {
|
||||
local target=keyboard.isDown("d") ? 1.45 : 0.0;
|
||||
local limit=(target>bucket.angle ? 0.872664626 : 1.745329252)/60.0;
|
||||
local delta=target-bucket.angle;
|
||||
if(delta>limit)delta=limit;else if(delta < -limit)delta=-limit;
|
||||
if(fabs(delta)>0.000001) {
|
||||
bucket.angle+=delta;
|
||||
fluidBucketUpdateColliderPoses(bucket.walls,bucket.colliders,bucket.angle);
|
||||
checked(solver.setColliders(bucket.colliders));
|
||||
}
|
||||
checked(solver.step(1.0/60.0,2));
|
||||
bucket.emitted+=checked(emitter.advance(solver,nozzle,1.0/60.0,2,0.25));
|
||||
if(solver.getParticleCount()>bucket.peakActive)bucket.peakActive=solver.getParticleCount();
|
||||
}
|
||||
else if(mode == 17) {
|
||||
local target=keyboard.isDown("d") ? 2.05 : 0.0;
|
||||
local maxSpeed=target>bottle.angle ? 0.785398163 : 1.570796327;
|
||||
local delta=target-bottle.angle; local limit=maxSpeed/60.0;
|
||||
if(delta>limit)delta=limit;else if(delta < -limit)delta=-limit;
|
||||
if(fabs(delta)>0.000001) {
|
||||
bottle.angle+=delta;bottle.pose=fluidBottlePose(bottle.angle,delta*60.0);
|
||||
checked(solver.updateSdfColliderPoses([bottle.pose]));
|
||||
}
|
||||
checked(solver.step(1.0/60.0,2));
|
||||
}
|
||||
else if(mode == 18) {
|
||||
checked(solver.step(1.0/60.0,2));
|
||||
local contacts=checked(solver.contacts()).len();
|
||||
if(contacts>karman.peakContacts)karman.peakContacts=contacts;
|
||||
if((simulationTime*60.0).tointeger()%6==0)fluidKarmanSampleWake(karman,solver);
|
||||
}
|
||||
else checked(solver.step(1.0 / 60.0, 2));
|
||||
if (mode == 14) updateOverlapTriggers(simulationTime);
|
||||
if (mode == 9) checked(solver.applyMaterialChannels());
|
||||
if (mode == 12) {
|
||||
checked(foamPool.advance(solver, 1.0/60.0, 4));
|
||||
checked(foamGenerator.advance(solver, foamPool, 1.0/60.0));
|
||||
}
|
||||
if (mode == 4) checked(emitter.advance(solver, nozzle, 1.0 / 60.0, 16, 0.5));
|
||||
if (mode == 6) checked(emitter.advanceMoving(solver, nozzle,
|
||||
movingPose(simulationTime), movingPose(simulationTime + 1.0 / 60.0),
|
||||
1.0 / 60.0, 16, 0.5, 1.0));
|
||||
simulationMs = (clock() - simulationStart) * 1000.0;
|
||||
frameWorkMs = simulationMs;
|
||||
simulatedThisFrame = true;
|
||||
simulationTime += 1.0 / 60.0;
|
||||
accumulator -= 1.0 / 60.0;
|
||||
if (accumulator >= 1.0 / 60.0) {
|
||||
droppedTime += accumulator;
|
||||
accumulator = 0.0;
|
||||
}
|
||||
dirty = true;
|
||||
// Do not add a synchronous reconstruction to an already
|
||||
// expensive simulation frame.
|
||||
if (simulationMs > 250.0)
|
||||
throw "Fluid simulation exceeded 250 ms budget";
|
||||
}
|
||||
} else {
|
||||
accumulator = 0.0;
|
||||
}
|
||||
// A paused/static frame reuses its texture. Reconstruction/readback is
|
||||
// currently synchronous, so submit it at most 30 times per second.
|
||||
if (dirty && !simulatedThisFrame && displayAccumulator >= presentationInterval && failure == "") {
|
||||
local reconstructionStart = clock();
|
||||
local presented = false;
|
||||
if (mode == 2) checked(surface.renderGasVolume(solver, 5.0));
|
||||
else if (mode == 12 || mode == 17) surface.renderVolume(solver);
|
||||
else {
|
||||
checked(surface.renderVolumeColorToTexture(solver, gfx, texture));
|
||||
presented = true;
|
||||
}
|
||||
if (mode == 12) {
|
||||
local foamStart = clock();
|
||||
checked(surface.compositeDiffuse(foamPool, 0.035, 0.9, 0.5));
|
||||
foamCompositeMs = (clock()-foamStart)*1000.0;
|
||||
}
|
||||
if(mode == 17) checked(surface.compositeConfiguredSceneRefraction(refractionBackground));
|
||||
reconstructionMs = (clock() - reconstructionStart) * 1000.0;
|
||||
local uploadStart = clock();
|
||||
if (!presented) checked(surface.copyToTexture(gfx, texture));
|
||||
uploadMs = (clock() - uploadStart) * 1000.0;
|
||||
frameWorkMs += reconstructionMs + uploadMs;
|
||||
dirty = false;
|
||||
displayAccumulator = 0.0;
|
||||
}
|
||||
if (frameWorkMs > 0.0) checkWorkBudget(frameWorkMs);
|
||||
} catch (error) {
|
||||
failure = error.tostring();
|
||||
paused = true;
|
||||
print("VOLUME_FLUID_STOPPED: " + failure + "\n");
|
||||
}
|
||||
};
|
||||
|
||||
eve_render = function() {
|
||||
gfx.clear();
|
||||
if(mode == 15 && mazeStateValid()) {
|
||||
foreach(wall in maze.walls) {
|
||||
local center=fluidMazePoint(wall.center,maze.angle);
|
||||
gfx.drawTexturedRectRotated(wheelTexture,480.0+center[0]*161.0,
|
||||
360.0-(center[1]-1.2)*161.0,wall.half[0]*322.0,wall.half[1]*322.0,
|
||||
-maze.angle*180.0/3.14159265359,0.22,0.28,0.34,1.0);
|
||||
}
|
||||
}
|
||||
if(mode == 16 && bucketStateValid()) {
|
||||
foreach(wall in bucket.walls) {
|
||||
local center=fluidBucketPoint(wall.center,bucket.angle);
|
||||
gfx.drawTexturedRectRotated(wheelTexture,480.0+center[0]*161.0,
|
||||
360.0-(center[1]-1.1)*161.0,wall.half[0]*322.0,wall.half[1]*322.0,
|
||||
-bucket.angle*180.0/3.14159265359,0.30,0.36,0.42,1.0);
|
||||
}
|
||||
}
|
||||
if(mode == 8 && wheelBody != null) {
|
||||
local angle=-2.0*atan2(wheelBody.getRotZ(),wheelBody.getRotW())*180.0/3.14159265359;
|
||||
drawMillStructure(angle,false);
|
||||
}
|
||||
gfx.drawTexturedRect(texture, 0.0, 0.0, 960.0, 720.0, 1.0, 1.0, 1.0, 1.0);
|
||||
if(mode == 8 && wheelBody != null) {
|
||||
local angle=-2.0*atan2(wheelBody.getRotZ(),wheelBody.getRotW())*180.0/3.14159265359;
|
||||
drawMillStructure(angle,true);
|
||||
}
|
||||
if(mode == 18)gfx.drawTexturedRect(obstacleTexture,440.0,325.0,70.0,70.0,1.0,1.0,1.0,1.0);
|
||||
if(mode == 17 && bottleStateValid()) {
|
||||
local body=fluidBottlePoint([0.0,0.68,0.0],bottle.angle);
|
||||
local neck=fluidBottlePoint([0.0,1.62,0.0],bottle.angle);
|
||||
gfx.drawTexturedRectRotated(wheelTexture,480.0+body[0]*183.0,360.0-(body[1]-0.95)*183.0,
|
||||
201.0,250.0,-bottle.angle*180.0/3.14159265359,0.42,0.65,0.76,0.18);
|
||||
gfx.drawTexturedRectRotated(wheelTexture,480.0+neck[0]*183.0,360.0-(neck[1]-0.95)*183.0,
|
||||
67.0,118.0,-bottle.angle*180.0/3.14159265359,0.48,0.72,0.82,0.22);
|
||||
}
|
||||
gfx.print("FLUID3D LAB", 24.0, 20.0, 0.8, 0.9, 1.0, 1.0, 1.4);
|
||||
gfx.print((paused ? "Paused" : "Running") + " | particles: " + solver.getParticleCount() +
|
||||
" | dropped seconds: " + format("%.2f", droppedTime), 24.0, 58.0, 0.8, 0.9, 1.0, 1.0, 1.0);
|
||||
gfx.print(format("Last work ms | Sim %.1f | Surface %.1f | Upload %.1f",
|
||||
simulationMs, reconstructionMs, uploadMs), 24.0, 90.0, 0.8, 0.9, 1.0, 1.0, 1.0);
|
||||
if (failure != "") gfx.print("Simulation stopped after error. See log.", 24.0, 125.0, 1.0, 0.4, 0.3, 1.0, 1.0);
|
||||
gfx.print("1 Water 2 Viscous 3 Smoke 4 Granular 5 Jet 6 Mesh 7 Moving", 24.0, 650.0, 0.7, 0.8, 0.9, 1.0, 1.0);
|
||||
gfx.print("8 Multiphase 9 Wheel 0 Mixing T Thermal S Solid F Foam D SDF Q Query M Maze B Bucket W Bottle K Wake", 24.0, 680.0, 0.7, 0.8, 0.9, 1.0, 0.72);
|
||||
if (mode == 10) gfx.print("Left: heating | Right: cooling | Color follows viscosity", 24.0, 150.0, 0.9, 0.8, 0.5, 1.0, 1.0);
|
||||
if (mode == 11) gfx.print("S Solid attachment: follows moving collider", 24.0, 150.0, 0.9, 0.8, 0.5, 1.0, 1.0);
|
||||
if (mode == 12) gfx.print("Foam: "+foamPool.getParticleCount()+" | Composite ms: "+format("%.2f",foamCompositeMs),
|
||||
24.0, 150.0, 0.9, 0.9, 1.0, 1.0, 1.0);
|
||||
if (mode == 13) gfx.print("D Moving SDF obstacle: transform-only updates, existing SSF surface", 24.0, 150.0, 0.9, 0.8, 0.5, 1.0, 1.0);
|
||||
if (mode == 14) gfx.print("Q Overlap triggers | red: "+overlapCounts[0]+" | yellow: "+overlapCounts[1],
|
||||
24.0,150.0,0.9,0.8,0.5,1.0,1.0);
|
||||
if (mode == 15) gfx.print("M FluidMaze | A/D rotate | completed: "+maze.finished.len()+
|
||||
" | contaminated: "+maze.colored.len(),24.0,150.0,0.9,0.8,0.5,1.0,1.0);
|
||||
if(mode == 16)gfx.print("B FaucetAndBucket | hold D to pour | emitted: "+bucket.emitted+
|
||||
" | active: "+solver.getParticleCount()+" | peak: "+bucket.peakActive,
|
||||
24.0,150.0,0.9,0.8,0.5,1.0,1.0);
|
||||
if(mode == 17)gfx.print("W WhiskeyBottle | hold D to pour | pose-only SDF | scene refraction",
|
||||
24.0,150.0,0.9,0.8,0.5,1.0,1.0);
|
||||
if(mode == 18)gfx.print("K FluidKarmanVortex | sphere wake | contacts: "+karman.peakContacts+
|
||||
" | peak curl: "+format("%.2f",karman.peakWakeCurl),24.0,150.0,0.9,0.8,0.5,1.0,1.0);
|
||||
if (mode == 2) gfx.print("Smoke: gas-only blurred thickness, CPU bounded path", 24.0, 150.0, 0.9, 0.8, 0.5, 1.0, 1.0);
|
||||
if(mode == 8) gfx.print("Wheel rad per sec: "+format("%.3f",wheelBody.getAngularVelocityZ())+
|
||||
" | contacts: "+wheelContacts,24.0,125.0,0.9,0.7,0.4,1.0,1.0);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
// Opt-in FluidMaze acceptance. Loading this file does not run it.
|
||||
function verifyVolumeFluidMaze() {
|
||||
paused = true;
|
||||
|
||||
local walls = fluidMazeWalls();
|
||||
local colliders = fluidMazeColliders(walls,0.0);
|
||||
fluidMazeUpdateColliderPoses(walls,colliders,0.45);
|
||||
local expected = fluidMazePoint(walls[5].center,0.45);
|
||||
local actual = colliders[5].center;
|
||||
if (fabs(actual[0]-expected[0])>0.00001 || fabs(actual[1]-expected[1])>0.00001 ||
|
||||
fabs(colliders[5].rotation[2]-sin(0.225))>0.00001)
|
||||
throw "Maze collider group transform mismatch";
|
||||
|
||||
local collisionDefinition = checked(fluids.volumeDefaults());
|
||||
collisionDefinition.settings.gravity = [0.0,0.0,0.0];
|
||||
collisionDefinition.settings.minimum = [-2.0,-1.0,-0.5];
|
||||
collisionDefinition.settings.maximum = [2.0,3.0,0.5];
|
||||
collisionDefinition.settings.iterations = 1;
|
||||
local collisionParticle = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
collisionParticle.position = expected;
|
||||
collisionParticle.material = clone collisionParticle.material;
|
||||
collisionParticle.material.cohesion = 0.0;
|
||||
collisionParticle.material.viscosity = 0.0;
|
||||
collisionDefinition.particles = [collisionParticle];
|
||||
collisionDefinition.colliders = [colliders[5]];
|
||||
local collisionSolver = checked(fluids.newVolumeSimulator(collisionDefinition));
|
||||
checked(collisionSolver.step(1.0/120.0,1));
|
||||
local contacts = checked(collisionSolver.contacts());
|
||||
if (contacts.len()!=1 || contacts[0].colliderLabel!=walls[5].label)
|
||||
throw "Rotated maze wall did not produce labelled contact";
|
||||
|
||||
local triggerDefinition = checked(fluids.volumeDefaults());
|
||||
triggerDefinition.settings.gravity = [0.0,0.0,0.0];
|
||||
triggerDefinition.settings.minimum = [-2.0,-1.0,-0.5];
|
||||
triggerDefinition.settings.maximum = [2.0,3.0,0.5];
|
||||
local triggerPrototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
triggerPrototype.material = clone triggerPrototype.material;
|
||||
triggerPrototype.material.cohesion = 0.0;
|
||||
triggerPrototype.material.viscosity = 0.0;
|
||||
local triggerParticles = [];
|
||||
foreach (position in [[0.82,2.02,0.0],[-0.82,1.24,0.0],[0.0,0.65,0.0]]) {
|
||||
local particle = clone triggerPrototype;
|
||||
particle.material = clone triggerPrototype.material;
|
||||
particle.position = position;
|
||||
triggerParticles.append(particle);
|
||||
}
|
||||
triggerDefinition.particles = triggerParticles;
|
||||
local triggerScene = {solver=checked(fluids.newVolumeSimulator(triggerDefinition)),angle=0.0,
|
||||
finished={},colored={}};
|
||||
local first = fluidMazeApplyTriggers(triggerScene);
|
||||
if (first.color0!=1 || first.color1!=1 || first.finish!=1 ||
|
||||
first.coloredTotal!=2 || first.finishedTotal!=1)
|
||||
throw "Maze color/finish trigger counts mismatch";
|
||||
local colored = checked(triggerScene.solver.snapshot()).particles;
|
||||
triggerScene.angle = 1.4;
|
||||
local second = fluidMazeApplyTriggers(triggerScene);
|
||||
local preserved = checked(triggerScene.solver.snapshot()).particles;
|
||||
for (local channel=0;channel<4;++channel)
|
||||
if (preserved[0].color[channel]!=colored[0].color[channel] ||
|
||||
preserved[1].color[channel]!=colored[1].color[channel])
|
||||
throw "Maze contamination did not persist outside trigger";
|
||||
if (preserved[0].color[0]<0.9 || preserved[1].color[2]<0.9)
|
||||
throw "Maze contamination color mismatch";
|
||||
if (second.finishedTotal < 1 || second.coloredTotal < 2)
|
||||
throw "Maze cumulative score lost prior particle identities";
|
||||
|
||||
rebuild(15);
|
||||
maze.angularSpeed = 0.7;
|
||||
local peakContacts = 0;
|
||||
for (local step = 0; step < 180; ++step) {
|
||||
maze.angle += maze.angularSpeed/120.0;
|
||||
if (step==90) maze.angularSpeed=-0.7;
|
||||
fluidMazeUpdateColliderPoses(maze.walls,maze.colliders,maze.angle);
|
||||
checked(solver.setColliders(maze.colliders));
|
||||
checked(solver.step(1.0/120.0,1));
|
||||
local contactCount = checked(solver.contacts()).len();
|
||||
if (contactCount>peakContacts) peakContacts=contactCount;
|
||||
fluidMazeApplyTriggers(maze,solver);
|
||||
}
|
||||
if (peakContacts<=0 || solver.getParticleCount()!=18)
|
||||
throw "Interactive maze did not retain bounded fluid/collider contacts";
|
||||
dirty = true;
|
||||
return "VOLUME_FLUID_MAZE_PASS walls="+walls.len()+" transformedContact="+
|
||||
contacts.len()+" colored="+first.coloredTotal+" finished="+first.finishedTotal+
|
||||
" persistent="+second.coloredTotal+" sceneFinished="+maze.finished.len()+
|
||||
" scenePeakContacts="+peakContacts;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Shared FluidMaze construction and trigger helpers. Loading this file is inert.
|
||||
function fluidMazePoint(localPoint, angle) {
|
||||
local c = cos(angle);
|
||||
local s = sin(angle);
|
||||
return [c * localPoint[0] - s * localPoint[1],
|
||||
1.2 + s * localPoint[0] + c * localPoint[1], localPoint[2]];
|
||||
}
|
||||
|
||||
function fluidMazeWalls() {
|
||||
return [
|
||||
{center=[-1.05,0.0,0.0],half=[0.06,1.1,0.34],label=101},
|
||||
{center=[1.05,0.0,0.0],half=[0.06,1.1,0.34],label=102},
|
||||
{center=[0.0,1.08,0.0],half=[1.1,0.06,0.34],label=103},
|
||||
{center=[-0.70,-1.08,0.0],half=[0.40,0.06,0.34],label=104},
|
||||
{center=[0.70,-1.08,0.0],half=[0.40,0.06,0.34],label=105},
|
||||
{center=[-0.30,0.64,0.0],half=[0.72,0.055,0.34],label=106},
|
||||
{center=[0.30,0.25,0.0],half=[0.72,0.055,0.34],label=107},
|
||||
{center=[-0.30,-0.14,0.0],half=[0.72,0.055,0.34],label=108},
|
||||
{center=[0.30,-0.53,0.0],half=[0.72,0.055,0.34],label=109},
|
||||
{center=[-0.30,-0.88,0.0],half=[0.72,0.055,0.34],label=110}
|
||||
];
|
||||
}
|
||||
|
||||
function fluidMazeUpdateColliderPoses(walls, colliders, angle) {
|
||||
local halfAngle = angle * 0.5;
|
||||
local rotation = [0.0, 0.0, sin(halfAngle), cos(halfAngle)];
|
||||
for (local i = 0; i < walls.len(); ++i) {
|
||||
colliders[i].center = fluidMazePoint(walls[i].center, angle);
|
||||
colliders[i].rotation = rotation;
|
||||
}
|
||||
}
|
||||
|
||||
function fluidMazeColliders(walls, angle) {
|
||||
local colliders = [];
|
||||
foreach (wall in walls) {
|
||||
local collider = checked(fluids.volumeColliderDefaults());
|
||||
collider.label = wall.label;
|
||||
collider.shape = 1;
|
||||
collider.halfExtent = wall.half;
|
||||
collider.friction = 0.08;
|
||||
colliders.append(collider);
|
||||
}
|
||||
fluidMazeUpdateColliderPoses(walls, colliders, angle);
|
||||
return colliders;
|
||||
}
|
||||
|
||||
function fluidMazeQueries(angle) {
|
||||
local halfAngle = angle * 0.5;
|
||||
local rotation = [0.0, 0.0, sin(halfAngle), cos(halfAngle)];
|
||||
local locals = [[0.82,0.82,0.0],[-0.82,0.04,0.0],[0.0,-0.55,0.0]];
|
||||
local sizes = [[0.28,0.28,0.68],[0.28,0.28,0.68],[2.0,1.00,0.68]];
|
||||
local queries = [];
|
||||
for (local i = 0; i < locals.len(); ++i)
|
||||
queries.append({type=1,center=fluidMazePoint(locals[i],angle),size=sizes[i],rotation=rotation,
|
||||
contactOffset=0.0,maxDistance=0.0,phaseMask=1,collisionFilter=4294901761});
|
||||
return queries;
|
||||
}
|
||||
|
||||
function fluidMazeScene() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 128;
|
||||
definition.settings.spacing = 0.09;
|
||||
definition.settings.minimum = [-1.55,-0.25,-0.45];
|
||||
definition.settings.maximum = [1.55,2.65,0.45];
|
||||
definition.settings.iterations = 3;
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
prototype.material = clone prototype.material;
|
||||
prototype.material.viscosity = 1.5;
|
||||
prototype.color = [0.08,0.62,1.0,1.0];
|
||||
for (local z = 0; z < 1; ++z) for (local y = 0; y < 3; ++y) for (local x = 0; x < 6; ++x) {
|
||||
local particle = clone prototype;
|
||||
particle.material = clone prototype.material;
|
||||
particle.position = [-0.225+x*0.09,2.02+y*0.09,0.0];
|
||||
definition.particles.append(particle);
|
||||
}
|
||||
local walls = fluidMazeWalls();
|
||||
local colliders = fluidMazeColliders(walls,0.0);
|
||||
definition.colliders = colliders;
|
||||
return {solver=checked(fluids.newVolumeSimulator(definition)),walls=walls,
|
||||
colliders=colliders,angle=0.0,angularSpeed=0.0,finished={},colored={}};
|
||||
}
|
||||
|
||||
function fluidMazeApplyTriggers(scene, solverOverride = null) {
|
||||
local activeSolver = solverOverride == null ? scene.solver : solverOverride;
|
||||
local queries = fluidMazeQueries(scene.angle);
|
||||
local colorCounts = checked(activeSolver.applyQueryColorsPreservingOutside(
|
||||
[queries[0],queries[1]],[[1.0,0.16,0.08,1.0],[0.9,0.15,1.0,1.0]],128));
|
||||
local colorHits = checked(activeSolver.queryBatch([queries[0],queries[1]],128));
|
||||
foreach (hit in colorHits) if (hit.distance < 0.0) scene.colored[hit.particleIndex.tostring()] <- true;
|
||||
local finishHits = checked(activeSolver.queryBatch([queries[2]],128));
|
||||
local immediateFinish = 0;
|
||||
foreach (hit in finishHits) if (hit.distance < 0.0) {
|
||||
scene.finished[hit.particleIndex.tostring()] <- true;
|
||||
++immediateFinish;
|
||||
}
|
||||
return {color0=colorCounts[0],color1=colorCounts[1],finish=immediateFinish,
|
||||
coloredTotal=scene.colored.len(),finishedTotal=scene.finished.len()};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Opt-in bounded mixing check. Leaves the resulting scene paused.
|
||||
function verifyVolumeFluidMixing() {
|
||||
paused = true;
|
||||
rebuild(9);
|
||||
local initial = checked(solver.snapshot()).particles;
|
||||
for (local i = 0; i < 120; ++i) {
|
||||
checked(solver.step(1.0 / 60.0, 4));
|
||||
checked(solver.applyMaterialChannels());
|
||||
}
|
||||
local final = checked(solver.snapshot()).particles;
|
||||
if (final.len() != 250) throw "Mixing changed particle count";
|
||||
local changed = 0;
|
||||
local materialChanged = 0;
|
||||
local cohesionChanged = 0;
|
||||
for (local i = 0; i < final.len(); ++i) {
|
||||
local p = final[i];
|
||||
if (fabs(p.color[0] - initial[i].color[0]) > 0.05) changed++;
|
||||
if (fabs(p.material.viscosity - initial[i].material.viscosity) > 1.0) materialChanged++;
|
||||
if (fabs(p.material.cohesion - initial[i].material.cohesion) > 0.03) cohesionChanged++;
|
||||
if (!(p.material.viscosity >= 2.0 && p.material.viscosity <= 50.0))
|
||||
throw "Mixed viscosity outside initial range";
|
||||
if (p.material.viscosity != p.data[0]) throw "Material mapping not applied";
|
||||
if (!(p.material.cohesion >= 0.5 && p.material.cohesion <= 2.0))
|
||||
throw "Mixed surface tension outside initial range";
|
||||
if (p.material.cohesion != p.data[1]) throw "Surface tension mapping not applied";
|
||||
for (local c = 0; c < 4; ++c)
|
||||
if (!(p.color[c] >= 0.0 && p.color[c] <= 1.0)) throw "Invalid mixed color";
|
||||
}
|
||||
if (changed < 50) throw "Insufficient color transfer";
|
||||
if (materialChanged < 50) throw "Insufficient material transfer";
|
||||
if (cohesionChanged < 50) throw "Insufficient surface-tension transfer";
|
||||
dirty = true;
|
||||
return "VOLUME_FLUID_MIXING_PASS color=" + changed +
|
||||
" viscosity=" + materialChanged + " surfaceTension=" + cohesionChanged;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Opt-in runtime check for Fluid3DEmitter AddShape/RemoveShape/UpdateEmitterDistribution parity.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 8;
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
local emitterBase = checked(fluids.volumeEmissionDefaults());
|
||||
emitterBase.description.speed = 0.0;
|
||||
emitterBase.description.actorCapacity = 8;
|
||||
emitterBase.description.prototype.color = [0.2, 0.3, 0.4, 1.0];
|
||||
local left = checked(fluids.volumeEmissionFromEdge(0.0, 0.1, 0.0));
|
||||
left.description.origin = [-0.25, 1.0, 0.0];
|
||||
left.description.distribution[0].color = [1.0, 0.0, 0.0, 1.0];
|
||||
local right = checked(fluids.volumeEmissionFromEdge(0.0, 0.1, 0.0));
|
||||
right.description.origin = [0.25, 1.0, 0.0];
|
||||
right.description.distribution[0].color = [0.0, 0.0, 1.0, 1.0];
|
||||
local combined = checked(fluids.composeVolumeEmitterShapes(emitterBase, [left, right]));
|
||||
if (combined.description.distribution.len() != 2) throw "multi-shape composition count mismatch";
|
||||
checked(localSolver.emitBurst(combined, 2));
|
||||
local particles = checked(localSolver.snapshot()).particles;
|
||||
if (particles.len() != 2 || particles[0].position[0] > -0.249 || particles[1].position[0] < 0.249 ||
|
||||
particles[0].color[0] < 0.99 || particles[1].color[2] < 0.99)
|
||||
throw "multi-shape order, transform or color mismatch";
|
||||
local removed = checked(fluids.composeVolumeEmitterShapes(emitterBase, [right]));
|
||||
if (removed.description.distribution.len() != 1 || removed.description.distribution[0].position[0] < 0.249)
|
||||
throw "multi-shape remove/update mismatch";
|
||||
print("VOLUME_MULTI_SHAPE_EMITTER_PASS\n");
|
||||
@@ -0,0 +1,23 @@
|
||||
function verifyMulticolorFastPath() {
|
||||
paused = true;
|
||||
rebuild(9);
|
||||
dirty = false;
|
||||
checked(surface.configureAnisotropy(true));
|
||||
surface.renderVolume(solver);
|
||||
checked(surface.copyToTexture(gfx, texture));
|
||||
checked(surface.renderVolumeColorToTexture(solver, gfx, texture));
|
||||
|
||||
local fullStart = clock();
|
||||
for (local i = 0; i < 20; ++i) {
|
||||
surface.renderVolume(solver);
|
||||
checked(surface.copyToTexture(gfx, texture));
|
||||
}
|
||||
local fullMs = (clock() - fullStart) * 1000.0;
|
||||
local fastStart = clock();
|
||||
for (local i = 0; i < 20; ++i)
|
||||
checked(surface.renderVolumeColorToTexture(solver, gfx, texture));
|
||||
local fastMs = (clock() - fastStart) * 1000.0;
|
||||
if (!surface.usingGpu()) throw "multicolor fluid did not use GPU";
|
||||
if (fastMs >= fullMs) throw "device-local multicolor path was not faster";
|
||||
return "VOLUME_MULTICOLOR_FASTPATH_PASS fullMs=" + fullMs + " fastMs=" + fastMs;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
function verifyMulticolorSoak() {
|
||||
paused = true;
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 4096;
|
||||
definition.settings.spacing = 0.05;
|
||||
definition.settings.minimum = [-0.5, 0.0, -0.5];
|
||||
definition.settings.maximum = [0.5, 1.0, 0.5];
|
||||
definition.settings.iterations = 2;
|
||||
solver = checked(fluids.newVolumeSimulator(definition));
|
||||
local red = checked(fluids.volumeEmissionDefaults());
|
||||
red.description.shape = 2;
|
||||
red.description.extent = [0.17, 0.37, 0.37];
|
||||
red.description.actorCapacity = 4096;
|
||||
red.description.origin = [-0.2, 0.5, 0.0];
|
||||
red.description.speed = 0.0;
|
||||
red.description.useShapeColor = false;
|
||||
red.description.prototype.radii = [0.0, 0.0, 0.0];
|
||||
red.description.prototype.color = [0.95, 0.08, 0.03, 1.0];
|
||||
local blue = checked(fluids.volumeEmissionDefaults());
|
||||
blue.description.shape = 2;
|
||||
blue.description.extent = [0.17, 0.37, 0.37];
|
||||
blue.description.actorCapacity = 4096;
|
||||
blue.description.origin = [0.2, 0.5, 0.0];
|
||||
blue.description.speed = 0.0;
|
||||
blue.description.useShapeColor = false;
|
||||
blue.description.prototype.radii = [0.0, 0.0, 0.0];
|
||||
blue.description.prototype.color = [0.03, 0.2, 0.95, 0.75];
|
||||
local redCount = 2048;
|
||||
local blueCount = 2048;
|
||||
checked(solver.emitBurst(red, redCount));
|
||||
checked(solver.emitBurst(blue, blueCount));
|
||||
checked(surface.configureAnisotropy(true));
|
||||
surface.setCameraXYZ(1.7, 1.5, 2.5, 0.0, 0.45, 0.0, 48.0);
|
||||
checked(surface.renderVolumeColorToTexture(solver, gfx, texture));
|
||||
|
||||
local maximumMs = 0.0;
|
||||
local start = clock();
|
||||
for (local frame = 0; frame < 600; ++frame) {
|
||||
local frameStart = clock();
|
||||
checked(surface.renderVolumeColorToTexture(solver, gfx, texture));
|
||||
local frameMs = (clock() - frameStart) * 1000.0;
|
||||
if (frameMs > maximumMs) maximumMs = frameMs;
|
||||
}
|
||||
local elapsedMs = (clock() - start) * 1000.0;
|
||||
if (!surface.usingGpu()) throw "multicolor soak did not use GPU";
|
||||
if (elapsedMs > 60000.0) throw "multicolor soak exceeded bounded runtime";
|
||||
return "VOLUME_MULTICOLOR_SOAK_PASS particles=4096 frames=600 totalMs=" + elapsedMs +
|
||||
" averageMs=" + (elapsedMs / 600.0) + " maxMs=" + maximumMs;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Opt-in multiphase buoyancy acceptance. Runs bounded numerical scenes without rendering in the loop.
|
||||
function phaseHeights(particles) {
|
||||
local blueY=0.0,redY=0.0,blueCount=0,redCount=0;
|
||||
foreach(particle in particles) {
|
||||
if(particle.color[2]>particle.color[0]) {blueY+=particle.position[1];++blueCount;}
|
||||
else {redY+=particle.position[1];++redCount;}
|
||||
}
|
||||
if(blueCount==0||redCount==0)throw "Multiphase color groups missing";
|
||||
return {blue=blueY/blueCount,red=redY/redCount};
|
||||
}
|
||||
|
||||
function verifyMultiphaseFluidBuoyancy() {
|
||||
paused=true;
|
||||
rebuild(7);
|
||||
local initialState=checked(solver.snapshot());
|
||||
local initial=phaseHeights(initialState.particles);
|
||||
|
||||
local empty=checked(fluids.volumeDefaults());
|
||||
empty.settings.capacity=initialState.settings.capacity;
|
||||
local control=checked(fluids.newVolumeSimulator(empty));
|
||||
local controlState=clone initialState;
|
||||
controlState.particles=[];
|
||||
foreach(source in initialState.particles) {
|
||||
local particle=clone source;
|
||||
particle.material=clone source.material;
|
||||
particle.material.density=1000.0;
|
||||
controlState.particles.append(particle);
|
||||
}
|
||||
checked(control.restore(controlState));
|
||||
|
||||
for(local frame=0;frame<360;++frame) {
|
||||
checked(solver.step(1.0/60.0,2));
|
||||
checked(control.step(1.0/60.0,2));
|
||||
}
|
||||
local buoyant=phaseHeights(checked(solver.snapshot()).particles);
|
||||
local neutral=phaseHeights(checked(control.snapshot()).particles);
|
||||
if(buoyant.red>=buoyant.blue)
|
||||
throw "Dense phase did not sink below light phase";
|
||||
if(buoyant.red>=initial.red-0.1||buoyant.blue<=initial.blue+0.1)
|
||||
throw "Both density phases did not move in their expected relative directions";
|
||||
if(neutral.red<=neutral.blue)
|
||||
throw "Equal-density control inverted without a density contrast";
|
||||
dirty=true;
|
||||
return "VOLUME_MULTIPHASE_BUOYANCY_PASS initialLight="+initial.blue+
|
||||
" initialHeavy="+initial.red+" finalLight="+buoyant.blue+
|
||||
" finalHeavy="+buoyant.red+" neutralLight="+neutral.blue+
|
||||
" neutralHeavy="+neutral.red;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Opt-in check for the Fluid3D-compatible orthographic SSF projection.
|
||||
function verifyVolumeFluidOrthographicProjection() {
|
||||
local defaults = fluids.volumeDefaults();
|
||||
if (!defaults.ok) throw defaults.error.message;
|
||||
local created = fluids.newVolumeSimulator(defaults.value);
|
||||
if (!created.ok) throw created.error.message;
|
||||
local solver = created.value;
|
||||
local emission = fluids.volumeEmissionDefaults();
|
||||
if (!emission.ok) throw emission.error.message;
|
||||
local particle = clone emission.value.description.prototype;
|
||||
particle.position = [0.0, 1.0, 0.0];
|
||||
local emitted = solver.emit([particle]);
|
||||
if (!emitted.ok) throw emitted.error.message;
|
||||
|
||||
local surface = fluids.newSurfaceRenderer(64, 64);
|
||||
local configured = surface.configureProjection(true, 1.0);
|
||||
if (!configured.ok) throw configured.error.message;
|
||||
surface.setCameraXYZ(0.0, 1.0, 3.0, 0.0, 1.0, 0.0, 45.0);
|
||||
surface.renderVolume(solver);
|
||||
if (!surface.usingGpu()) throw "Orthographic SSF did not use the GPU path";
|
||||
if (surface.configureProjection(true, 0.0).ok)
|
||||
throw "Invalid orthographic half size was accepted";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Opt-in acceptance probe. Loading this file has no side effects.
|
||||
function verifyVolumeFluidOverlapTriggers() {
|
||||
paused = true;
|
||||
rebuild(14);
|
||||
updateOverlapTriggers(0.0);
|
||||
if (overlapCounts.len() != 2 || overlapCounts[0] <= 0 || overlapCounts[1] <= 0)
|
||||
throw "Expected both overlap triggers to contain liquid";
|
||||
local state = checked(solver.snapshot());
|
||||
local red = 0, yellow = 0, cyan = 0;
|
||||
foreach (particle in state.particles) {
|
||||
if (particle.color[0] > 0.9 && particle.color[1] < 0.2) ++red;
|
||||
else if (particle.color[0] > 0.9 && particle.color[1] > 0.7) ++yellow;
|
||||
else if (particle.color[1] > 0.7 && particle.color[2] > 0.8) ++cyan;
|
||||
}
|
||||
if (red <= 0 || yellow <= 0 || cyan <= 0)
|
||||
throw "Overlap trigger colors were not observable";
|
||||
local before = state.particles[0].color;
|
||||
local bad = clone overlapQueries[0]; bad.rotation = [0.0, 0.0, 0.0, 2.0];
|
||||
if (solver.applyQueryColors([bad], [[1.0,0.0,0.0,1.0]], 0.0,1.0,1.0,1.0,1024).ok)
|
||||
throw "Invalid overlap trigger accepted";
|
||||
local after = checked(solver.snapshot()).particles[0].color;
|
||||
if (before[0] != after[0] || before[1] != after[1] || before[2] != after[2] || before[3] != after[3])
|
||||
throw "Failed overlap trigger changed particle colors";
|
||||
return "VOLUME_OVERLAP_TRIGGER_PASS red="+red+" yellow="+yellow+" cyan="+cyan;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, -10.0, 0.0];
|
||||
definition.settings.capacity = 4;
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
local a = clone prototype; a.position = [-0.2, 1.0, 0.0];
|
||||
local b = clone prototype; b.position = [0.2, 1.0, 0.0];
|
||||
definition.particles = [a, b];
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
local target = checked(fluids.volumeColliderDefaults());
|
||||
target.label = 71; target.center = [0.0, 1.0, 0.0]; target.radius = 0.1;
|
||||
checked(localSolver.setColliders([target]));
|
||||
if (checked(localSolver.bindStaticParticles([0], 71, false)) != 1)
|
||||
throw "Static particle attachment was not installed";
|
||||
target.center = [0.25, 1.2, 0.0];
|
||||
checked(localSolver.setColliders([target]));
|
||||
checked(localSolver.step(1.0 / 120.0, 1));
|
||||
local attached = checked(localSolver.snapshot());
|
||||
if (attached.version != 20 || attached.attachments.len() != 1 ||
|
||||
attached.attachments[0].constrainOrientation || attached.particles[0].material.phase != 0 ||
|
||||
fabs(attached.particles[0].position[0] - 0.05) > 0.00001 ||
|
||||
fabs(attached.particles[0].position[1] - 1.2) > 0.00001)
|
||||
throw "Static particle attachment state mismatch";
|
||||
if (checked(localSolver.unbindStaticParticles([0])) != 1)
|
||||
throw "Static particle attachment was not removed";
|
||||
return "VOLUME_PARTICLE_ATTACHMENT_PASS";
|
||||
@@ -0,0 +1,20 @@
|
||||
// Opt-in check for Fluid3DParticlePicker + Fluid3DParticleDragger behavior.
|
||||
function verifyVolumeFluidParticleDrag() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
local particle = clone checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.0, 1.0, 0.0];
|
||||
particle.velocity = [1.0, 0.0, 0.0];
|
||||
definition.particles = [particle];
|
||||
local source = checked(fluids.newVolumeSimulator(definition));
|
||||
local hits = checked(source.raycast(-1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 4.0, 1, 15));
|
||||
if (hits.len() != 1 || hits[0].particleIndex != 0)
|
||||
throw "Particle picker did not return the nearest particle";
|
||||
checked(source.applyParticleDrag(hits[0].particleIndex, 1.0, 1.0, 0.0, 12.0, 2.0, 0.01));
|
||||
local state = checked(source.snapshot());
|
||||
if (fabs(state.particles[0].velocity[0] - 1.1) > 0.00001)
|
||||
throw "Particle drag spring response is incorrect";
|
||||
if (source.applyParticleDrag(1, 1.0, 1.0, 0.0, 12.0, 2.0, 0.01).ok)
|
||||
throw "Stale drag index was accepted";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 2;
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
local a = clone prototype; a.position = [0.5, 1.0, 0.5]; a.actorGroup = 7;
|
||||
a.orientation = [0.0, 0.0, 0.70710678, 0.70710678];
|
||||
local b = clone prototype; b.position = [-0.5, 1.0, 0.0]; b.actorGroup = 8;
|
||||
definition.particles = [a, b];
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
local frames = checked(localSolver.debugParticleFrames(7, 2.0, 1));
|
||||
if (frames.len() != 1 || frames[0].particleIndex != 0 ||
|
||||
fabs(frames[0].x[0] - 0.5) > 0.00001 || fabs(frames[0].x[1] - 3.0) > 0.00001 ||
|
||||
fabs(frames[0].y[0] + 1.5) > 0.00001 || fabs(frames[0].z[2] - 2.5) > 0.00001)
|
||||
throw "Particle frame debug axes mismatch";
|
||||
return "VOLUME_PARTICLE_FRAME_PASS";
|
||||
@@ -0,0 +1,18 @@
|
||||
// Opt-in check for Fluid3DParticleRenderer radiusScale/particleColor semantics.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
definition.settings.capacity = 2;
|
||||
local particle = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.0, 1.0, 0.0];
|
||||
particle.velocity = [2.0, 0.0, 0.0];
|
||||
particle.radii = [0.1, 0.2, 0.3];
|
||||
particle.color = [0.8, 0.5, 0.25, 0.75];
|
||||
particle.actorGroup = 9;
|
||||
definition.particles = [particle];
|
||||
local solver = checked(fluids.newVolumeSimulator(definition));
|
||||
checked(solver.step(1.0 / 120.0, 1));
|
||||
local rows = checked(solver.particleImpostors(9, 2.0, 0.5, 0.25, 1.0, 0.5, 0.5, 1));
|
||||
if (rows.len() != 1 || abs(rows[0].position[0] - (1.0 / 120.0)) > 0.0001 ||
|
||||
abs(rows[0].scale[0] - 0.2) > 0.0001 || abs(rows[0].scale[2] - 0.6) > 0.0001 ||
|
||||
abs(rows[0].color[0] - 0.4) > 0.0001 || abs(rows[0].color[3] - 0.375) > 0.0001)
|
||||
throw "particle impostor projection mismatch";
|
||||
@@ -0,0 +1,23 @@
|
||||
// Opt-in check for Fluid3DInstancedParticleRenderer-style actor instance data.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
local first = clone prototype;
|
||||
first.position = [0.0, 1.0, 0.0]; first.velocity = [1.0, 0.0, 0.0];
|
||||
first.radii = [0.1, 0.2, 0.3]; first.actorGroup = 7; first.color = [0.2, 0.3, 0.4, 0.5];
|
||||
local second = clone prototype;
|
||||
second.position = [0.5, 1.0, 0.0]; second.actorGroup = 8;
|
||||
definition.particles = [first, second];
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
checked(localSolver.step(1.0 / 120.0, 1));
|
||||
local instances = checked(localSolver.particleInstances(7, 2.0, 3.0, 4.0, 0.5, 1));
|
||||
if (instances.len() != 1 || instances[0].particleIndex != 0 ||
|
||||
fabs(instances[0].position[0] - 1.0 / 240.0) > 0.00001 ||
|
||||
fabs(instances[0].scale[0] - 0.2) > 0.00001 ||
|
||||
fabs(instances[0].scale[1] - 0.6) > 0.00001 ||
|
||||
fabs(instances[0].scale[2] - 1.2) > 0.00001 ||
|
||||
fabs(instances[0].color[3] - 0.5) > 0.00001)
|
||||
throw "Particle instance data mismatch";
|
||||
if (localSolver.particleInstances(7, 1.0, 1.0, 1.0, 1.0, 0).ok)
|
||||
throw "Particle instance budget was not enforced";
|
||||
return "VOLUME_PARTICLE_INSTANCES_PASS";
|
||||
@@ -0,0 +1,242 @@
|
||||
// Opt-in check for the paused example. Loading this file does not run it.
|
||||
function verifyVolumeFluidQueryMaterial() {
|
||||
paused = true;
|
||||
rebuild(0);
|
||||
local query = function() { return checked(solver.overlapSphere(-1.0, 0.12, -0.42, 0.01)); };
|
||||
local hits = query();
|
||||
if (hits.len() != 1) throw "Expected one particle in brush";
|
||||
local boxed = checked(solver.overlapBox(-1.0, 0.12, -0.42,
|
||||
0.01, 0.01, 0.01, 0.0, 0.0, 0.70710678, 0.70710678));
|
||||
if (boxed.len() != 1) throw "Rotated box query failed";
|
||||
if (solver.overlapBox(0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 2.0).ok)
|
||||
throw "Invalid box rotation accepted";
|
||||
local ray = checked(solver.raycast(-1.0, 0.12, -1.0, 0.0, 0.0, 1.0, 2.0, 2, 1));
|
||||
if (ray.len()!=2 || ray[0].particleIndex!=0 || ray[0].distance<=0.0 || ray[0].normal[2]>=0.0)
|
||||
throw "Bounded ray query mismatch";
|
||||
if (ray[0].particle.position[2]!=-0.42 || ray[1].distance<=ray[0].distance)
|
||||
throw "Ray hits are not owning and distance sorted";
|
||||
if (solver.raycast(0.0,0.0,0.0,0.0,0.0,0.0,1.0,1,15).ok ||
|
||||
solver.raycast(0.0,0.0,0.0,0.0,0.0,1.0,1.0,-1,15).ok ||
|
||||
solver.raycast(0.0,0.0,0.0,0.0,0.0,1.0,1.0,1,16).ok)
|
||||
throw "Invalid ray query accepted";
|
||||
local distances = checked(solver.querySphere(-1.0,0.12,-0.42,0.0,0.02,0.0,4,1));
|
||||
if (distances.len()!=1 || distances[0].particleIndex!=0 ||
|
||||
fabs(distances[0].distance+0.08)>0.00001 ||
|
||||
fabs(distances[0].queryPoint[1]-0.14)>0.00001)
|
||||
throw "Signed sphere distance query mismatch";
|
||||
distances[0].particle.position[0]=9.0;
|
||||
if (query()[0].position[0]!=-1.0) throw "Distance query leaked mutable state";
|
||||
if (solver.querySphere(0.0,0.0,0.0,-1.0,0.0,1.0,1,15).ok ||
|
||||
solver.querySphere(0.0,0.0,0.0,1.0,-1.0,1.0,1,15).ok ||
|
||||
solver.querySphere(0.0,0.0,0.0,1.0,0.0,1.0,1,16).ok)
|
||||
throw "Invalid sphere distance query accepted";
|
||||
local boxDistances = checked(solver.queryBox(-1.0,0.12,-0.42,
|
||||
0.01,0.01,0.01, 0.0,0.0,0.70710678,0.70710678, 0.02,0.0,4,1));
|
||||
if (boxDistances.len()!=1 || boxDistances[0].particleIndex!=0 ||
|
||||
fabs(boxDistances[0].distance+0.09)>0.00001 ||
|
||||
fabs(boxDistances[0].queryPoint[1]-0.09)>0.00001 || boxDistances[0].normal[1]>=0.0)
|
||||
throw "Oriented box distance query mismatch";
|
||||
boxDistances[0].particle.position[0]=9.0;
|
||||
if (query()[0].position[0]!=-1.0) throw "Box distance query leaked mutable state";
|
||||
if (solver.queryBox(0.0,0.0,0.0,-1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,1.0,1,15).ok ||
|
||||
solver.queryBox(0.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,2.0,0.0,1.0,1,15).ok)
|
||||
throw "Invalid box distance query accepted";
|
||||
local batch = checked(solver.queryBatch([
|
||||
{type=0,center=[-1.0,0.12,-0.42],size=[0.0,0.0,0.0],rotation=[0.0,0.0,0.0,1.0],contactOffset=0.0,maxDistance=0.0,phaseMask=1,collisionFilter=4294901761},
|
||||
{type=1,center=[-1.0,0.12,-0.42],size=[0.02,0.02,0.02],rotation=[0.0,0.0,0.70710678,0.70710678],contactOffset=0.0,maxDistance=0.0,phaseMask=1,collisionFilter=4294901761},
|
||||
{type=2,center=[-1.0,0.12,-1.0],size=[-1.0,0.12,1.0],rotation=[0.0,0.0,0.0,1.0],contactOffset=0.0,maxDistance=0.0,phaseMask=1,collisionFilter=4294901761}
|
||||
], 2));
|
||||
if (batch.len()!=4 || batch[0].queryIndex!=0 || batch[1].queryIndex!=1 ||
|
||||
batch[2].queryIndex!=2 || batch[2].particleIndex!=0 || batch[3].particleIndex<=0)
|
||||
throw "Mixed query batch mismatch";
|
||||
checked(solver.setSimplexes([{particleIndices=[0,1,2],size=3}]));
|
||||
local simplexHits = checked(solver.querySimplexes([{
|
||||
type=0,center=[-1.0,0.12,-0.42],size=[0.0,0.0,0.0],rotation=[0.0,0.0,0.0,1.0],
|
||||
contactOffset=0.0,maxDistance=1.0,phaseMask=1,collisionFilter=4294901761
|
||||
}], 2));
|
||||
if (simplexHits.len()!=1 || simplexHits[0].simplexIndex!=0 || simplexHits[0].queryIndex!=0 ||
|
||||
fabs(simplexHits[0].simplexBary[0]+simplexHits[0].simplexBary[1]+simplexHits[0].simplexBary[2]-1.0)>0.00001)
|
||||
throw "Simplex barycentric query mismatch";
|
||||
local ellipsoidDefinition = checked(fluids.volumeDefaults());
|
||||
ellipsoidDefinition.settings.spacing = 0.2;
|
||||
local ellipsoid = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
ellipsoid.position = [0.0,1.0,0.0];
|
||||
ellipsoid.radii = [0.2,0.05,0.05];
|
||||
ellipsoid.orientation = [0.0,0.0,0.70710678,0.70710678];
|
||||
ellipsoidDefinition.particles = [ellipsoid];
|
||||
local ellipsoidSolver = checked(fluids.newVolumeSimulator(ellipsoidDefinition));
|
||||
local ellipsoidDistance = checked(ellipsoidSolver.querySphere(0.0,1.3,0.0,0.0,0.0,1.0,1,15));
|
||||
if (ellipsoidDistance.len()!=1 || fabs(ellipsoidDistance[0].distance-0.1)>0.00001)
|
||||
throw "Oriented ellipsoid distance mismatch";
|
||||
local ellipsoidRay = checked(ellipsoidSolver.raycast(0.0,0.7,0.0,0.0,1.0,0.0,1.0,1,15));
|
||||
if (ellipsoidRay.len()!=1 || fabs(ellipsoidRay[0].distance-0.1)>0.00001 || ellipsoidRay[0].normal[1]>=0.0)
|
||||
throw "Oriented ellipsoid ray mismatch";
|
||||
local material = clone hits[0].material;
|
||||
hits[0].material.phase = 3;
|
||||
if (query()[0].material.phase != 0) throw "Query leaked mutable state";
|
||||
material.phase = 3;
|
||||
checked(solver.paintSphere(-1.0, 0.12, -0.42, 0.01, material));
|
||||
if (query()[0].material.phase != 3) throw "Brush did not freeze particle";
|
||||
local outside = checked(solver.overlapSphere(-0.88, 0.12, -0.42, 0.01));
|
||||
if (outside.len() != 1 || outside[0].material.phase != 0) throw "Brush changed outside particle";
|
||||
material.viscosity = -1.0;
|
||||
if (solver.paintSphere(-1.0, 0.12, -0.42, 0.01, material).ok) throw "Invalid material accepted";
|
||||
if (query()[0].material.viscosity != 2.0) throw "Failed brush mutated state";
|
||||
material.viscosity = 2.0;
|
||||
material.unrecognized <- 1;
|
||||
if (solver.paintSphere(-1.0, 0.12, -0.42, 0.01, material).ok) throw "Unknown field accepted";
|
||||
delete material.unrecognized;
|
||||
material.phase = 0;
|
||||
checked(solver.paintSphere(-1.0, 0.12, -0.42, 0.01, material));
|
||||
if (query()[0].material.phase != 0) throw "Brush did not melt particle";
|
||||
if (solver.overlapSphere(0.0, 0.0, 0.0, -1.0).ok) throw "Negative radius accepted";
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
local particle = query()[0];
|
||||
particle.data = [20.0, 1.5, 0.0, 0.0];
|
||||
definition.particles = [particle];
|
||||
local mapped = checked(fluids.newVolumeSimulator(definition));
|
||||
checked(mapped.applyMaterialChannels());
|
||||
local mappedMaterial = checked(mapped.snapshot()).particles[0].material;
|
||||
if (mappedMaterial.viscosity != 20.0 || mappedMaterial.cohesion != 1.5)
|
||||
throw "Material channel mapping failed";
|
||||
local filterDefinition = checked(fluids.volumeDefaults());
|
||||
filterDefinition.settings.gravity = [0.0,0.0,0.0]; filterDefinition.settings.iterations = 2;
|
||||
local filterA = clone particle; local filterB = clone particle;
|
||||
filterA.position = [0.0,1.0,0.0]; filterB.position = [0.0,1.0,0.0];
|
||||
filterA.actorGroup = 1; filterB.actorGroup = 2;
|
||||
filterA.collisionFilter = 131073; filterB.collisionFilter = 131074;
|
||||
filterDefinition.particles = [filterA,filterB];
|
||||
local filtered = checked(fluids.newVolumeSimulator(filterDefinition)); checked(filtered.step(1.0/120.0,1));
|
||||
local filteredState = checked(filtered.snapshot()).particles;
|
||||
local filteredDelta = [filteredState[0].position[0]-filteredState[1].position[0],
|
||||
filteredState[0].position[1]-filteredState[1].position[1],filteredState[0].position[2]-filteredState[1].position[2]];
|
||||
if (filteredDelta[0]*filteredDelta[0]+filteredDelta[1]*filteredDelta[1]+filteredDelta[2]*filteredDelta[2] != 0.0)
|
||||
throw "One-way particle filters interacted";
|
||||
filterB.collisionFilter = 65538; filterDefinition.particles = [filterA,filterB];
|
||||
local reciprocal = checked(fluids.newVolumeSimulator(filterDefinition)); checked(reciprocal.step(1.0/120.0,1));
|
||||
local reciprocalState = checked(reciprocal.snapshot()).particles;
|
||||
local reciprocalDelta = [reciprocalState[0].position[0]-reciprocalState[1].position[0],
|
||||
reciprocalState[0].position[1]-reciprocalState[1].position[1],reciprocalState[0].position[2]-reciprocalState[1].position[2]];
|
||||
if (reciprocalDelta[0]*reciprocalDelta[0]+reciprocalDelta[1]*reciprocalDelta[1]+
|
||||
reciprocalDelta[2]*reciprocalDelta[2] < 0.000001)
|
||||
throw "Reciprocal particle filters did not interact";
|
||||
filterB.actorGroup = 1; filterB.collisionFilter = 131074; filterA.selfCollide = true; filterB.selfCollide = false;
|
||||
filterDefinition.particles = [filterA,filterB];
|
||||
local noSelfCollision = checked(fluids.newVolumeSimulator(filterDefinition));checked(noSelfCollision.step(1.0/120.0,1));
|
||||
local noSelfState = checked(noSelfCollision.snapshot()).particles;
|
||||
local noSelfDelta = [noSelfState[0].position[0]-noSelfState[1].position[0],
|
||||
noSelfState[0].position[1]-noSelfState[1].position[1],noSelfState[0].position[2]-noSelfState[1].position[2]];
|
||||
if (noSelfDelta[0]*noSelfDelta[0]+noSelfDelta[1]*noSelfDelta[1]+noSelfDelta[2]*noSelfDelta[2] != 0.0)
|
||||
throw "Same actor group ignored self-collision flag";
|
||||
local colliderDefinition = checked(fluids.volumeDefaults());
|
||||
colliderDefinition.settings.gravity = [0.0,0.0,0.0]; colliderDefinition.settings.iterations = 1;
|
||||
local colliderParticle = clone particle; colliderParticle.position = [0.0,1.0,0.0];
|
||||
colliderParticle.collisionFilter = 131073; colliderDefinition.particles = [colliderParticle];
|
||||
local collider = checked(fluids.volumeColliderDefaults()); collider.center = [0.0,1.0,0.0];
|
||||
collider.collisionFilter = 131074; colliderDefinition.colliders = [collider];
|
||||
local ignoredCollider = checked(fluids.newVolumeSimulator(colliderDefinition));
|
||||
checked(ignoredCollider.step(1.0/120.0,1));
|
||||
local ignoredPosition = checked(ignoredCollider.snapshot()).particles[0].position;
|
||||
local ignoredDelta = [ignoredPosition[0],ignoredPosition[1]-1.0,ignoredPosition[2]];
|
||||
if (ignoredDelta[0]*ignoredDelta[0]+ignoredDelta[1]*ignoredDelta[1]+ignoredDelta[2]*ignoredDelta[2] != 0.0)
|
||||
throw "One-way collider filter projected particle";
|
||||
collider.collisionFilter = 65538; colliderDefinition.colliders = [collider];
|
||||
local matchingCollider = checked(fluids.newVolumeSimulator(colliderDefinition));
|
||||
checked(matchingCollider.step(1.0/120.0,1));
|
||||
local matchingPosition = checked(matchingCollider.snapshot()).particles[0].position;
|
||||
local matchingDelta = [matchingPosition[0],matchingPosition[1]-1.0,matchingPosition[2]];
|
||||
if (matchingDelta[0]*matchingDelta[0]+matchingDelta[1]*matchingDelta[1]+matchingDelta[2]*matchingDelta[2] < 0.000001)
|
||||
throw "Reciprocal collider filters did not interact";
|
||||
local atmosphereDefinition = checked(fluids.volumeDefaults());
|
||||
atmosphereDefinition.settings.gravity = [0.0,0.0,0.0]; atmosphereDefinition.settings.iterations = 1;
|
||||
local atmosphereA = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
local atmosphereB = clone atmosphereA; atmosphereA.material = clone atmosphereA.material; atmosphereB.material = clone atmosphereB.material;
|
||||
atmosphereA.position = [-0.075,1.0,0.0]; atmosphereB.position = [0.075,1.0,0.0];
|
||||
atmosphereA.material.viscosity = 0.0; atmosphereB.material.viscosity = 0.0;
|
||||
atmosphereA.material.cohesion = 0.0; atmosphereB.material.cohesion = 0.0;
|
||||
atmosphereA.material.atmosphericPressure = 10.0; atmosphereB.material.atmosphericPressure = 10.0;
|
||||
atmosphereDefinition.particles = [atmosphereA,atmosphereB];
|
||||
local atmosphere = checked(fluids.newVolumeSimulator(atmosphereDefinition));checked(atmosphere.step(1.0/120.0,1));
|
||||
local atmosphereState = checked(atmosphere.snapshot()).particles;
|
||||
if (atmosphereState[0].velocity[0] <= 0.0 || atmosphereState[1].velocity[0] >= 0.0)
|
||||
throw "Atmospheric pressure did not compress the free surface";
|
||||
local windDefinition=checked(fluids.volumeDefaults());windDefinition.settings.gravity=[0.0,0.0,0.0];windDefinition.settings.iterations=1;
|
||||
local windParticle=checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
windParticle.position=[0.0,1.0,0.0];windParticle.material.cohesion=0.0;
|
||||
windParticle.material.viscosity=0.0;windParticle.material.drag=60.0;windDefinition.particles=[windParticle];
|
||||
local windSolver=checked(fluids.newVolumeSimulator(windDefinition));checked(windSolver.setParticleWinds([[6.0,0.0,0.0]]));
|
||||
if (windSolver.setParticleWinds([[0.0,0.0,0.0],[0.0,0.0,0.0]]).ok) throw "Invalid wind count accepted";
|
||||
if (windSolver.step(-1.0,1).ok) throw "Invalid wind step accepted";
|
||||
checked(windSolver.step(1.0/120.0,1));local windDriven=checked(windSolver.snapshot()).particles[0].velocity[0];
|
||||
if (windDriven<=2.0 || windDriven>=3.0) throw "Relative wind drag did not accelerate particle";
|
||||
checked(windSolver.step(1.0/120.0,1));
|
||||
if (checked(windSolver.snapshot()).particles[0].velocity[0]>=windDriven) throw "Wind did not clear after successful step";
|
||||
local zoneSolver=checked(fluids.newVolumeSimulator(windDefinition));
|
||||
local ambient=checked(fluids.volumeWindZoneDefaults());ambient.direction=[0.0,1.0,0.0];ambient.intensity=2.0;
|
||||
local spherical=checked(fluids.volumeWindZoneDefaults());spherical.type=1;spherical.center=[-1.0,1.0,0.0];
|
||||
spherical.radius=2.0;spherical.intensity=4.0;spherical.radial=true;
|
||||
checked(zoneSolver.accumulateWindZones([ambient,spherical],0.0));
|
||||
local invalidZone=clone spherical;invalidZone.radius=0.0;
|
||||
if(zoneSolver.accumulateWindZones([invalidZone],0.0).ok)throw "Invalid spherical wind radius accepted";
|
||||
checked(zoneSolver.step(1.0/120.0,1));local zoneVelocity=checked(zoneSolver.snapshot()).particles[0].velocity;
|
||||
if(zoneVelocity[0]<=0.0||zoneVelocity[1]<=0.0||abs(zoneVelocity[0]/zoneVelocity[1]-1.5)>0.001)
|
||||
throw "Ambient/spherical wind-zone accumulation mismatch";
|
||||
local smoothingDefinition = checked(fluids.volumeDefaults());
|
||||
if (smoothingDefinition.version != 20) throw "Smoothing snapshot version mismatch";
|
||||
smoothingDefinition.settings.gravity = [0.0,0.0,0.0]; smoothingDefinition.settings.iterations = 1;
|
||||
local smoothingA = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
local smoothingB = clone smoothingA;smoothingA.material=clone smoothingA.material;smoothingB.material=clone smoothingB.material;
|
||||
smoothingA.position=[-0.125,1.0,0.0];smoothingB.position=[0.125,1.0,0.0];
|
||||
smoothingA.velocity=[1.0,0.0,0.0];smoothingB.velocity=[-1.0,0.0,0.0];
|
||||
smoothingA.material.cohesion=0.0;smoothingB.material.cohesion=0.0;
|
||||
smoothingA.material.viscosity=100.0;smoothingB.material.viscosity=100.0;
|
||||
smoothingA.material.smoothing=1.0;smoothingB.material.smoothing=4.0;
|
||||
smoothingDefinition.particles=[smoothingA,smoothingB];
|
||||
local smoothingSolver=checked(fluids.newVolumeSimulator(smoothingDefinition));checked(smoothingSolver.step(1.0/120.0,1));
|
||||
local smoothingState=checked(smoothingSolver.snapshot()).particles;
|
||||
if (smoothingState[0].velocity[0] >= 0.999 || fabs(smoothingState[0].velocity[0]+smoothingState[1].velocity[0])>0.00001)
|
||||
throw "Per-particle smoothing did not expand symmetric SPH support";
|
||||
smoothingDefinition.particles[0].material.smoothing=4.01;
|
||||
if (fluids.newVolumeSimulator(smoothingDefinition).ok) throw "Invalid smoothing accepted";
|
||||
local granularDefinition = checked(fluids.volumeDefaults());
|
||||
granularDefinition.settings.gravity = [0.0,0.0,0.0];
|
||||
local granularSolver = checked(fluids.newVolumeSimulator(granularDefinition));
|
||||
local granularEmission = checked(fluids.volumeEmissionDefaults());
|
||||
if (granularEmission.version != 10 || granularEmission.description.granularRadiusRandomness != 0.0 ||
|
||||
granularEmission.description.prototype.material.smoothing != 2.0 ||
|
||||
granularEmission.description.prototype.material.rollingContacts ||
|
||||
granularEmission.description.prototype.material.rollingFriction != 0.0 ||
|
||||
abs(granularEmission.description.prototype.material.dynamicFriction - 0.2) > 0.00001 ||
|
||||
abs(granularEmission.description.prototype.material.staticFriction - 0.2) > 0.00001 ||
|
||||
granularEmission.description.prototype.angularVelocity[0] != 0.0 ||
|
||||
granularEmission.description.prototype.angularVelocity[1] != 0.0 ||
|
||||
granularEmission.description.prototype.angularVelocity[2] != 0.0)
|
||||
throw "Granular randomness schema/default mismatch";
|
||||
granularEmission.description.origin = [0.0,1.0,0.0];
|
||||
granularEmission.description.extent = [0.0,0.0,0.0];
|
||||
granularEmission.description.prototype.material.phase = 2;
|
||||
granularEmission.description.granularRadiusRandomness = 100.0;
|
||||
checked(granularSolver.emitBurst(granularEmission,8));
|
||||
local granularParticles = checked(granularSolver.snapshot()).particles;
|
||||
local granularVaried = false;
|
||||
foreach (granularParticle in granularParticles) {
|
||||
if (granularParticle.radii[0] < 0.001 || granularParticle.radii[0] > 0.051001 ||
|
||||
granularParticle.radii[0] != granularParticle.radii[1] || granularParticle.radii[1] != granularParticle.radii[2])
|
||||
throw "Granular radius outside deterministic isotropic bounds";
|
||||
if (granularParticle.radii[0] < 0.049) granularVaried = true;
|
||||
}
|
||||
if (!granularVaried) throw "Granular radius randomness produced no variation";
|
||||
local sdfDefinition=checked(fluids.volumeDefaults());sdfDefinition.settings.gravity=[0.0,0.0,0.0];
|
||||
sdfDefinition.settings.iterations=1;
|
||||
local sdfParticle=checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
sdfParticle.position=[0.45,0.1,0.0];sdfParticle.material=clone sdfParticle.material;
|
||||
sdfParticle.material.cohesion=0.0;sdfParticle.material.viscosity=0.0;sdfDefinition.particles=[sdfParticle];
|
||||
local sdfSolver=checked(fluids.newVolumeSimulator(sdfDefinition));
|
||||
local sdfCollider=checked(fluids.volumeSdfColliderDefaults());sdfCollider.label=88;sdfCollider.friction=0.0;
|
||||
checked(sdfSolver.setSdfColliders([sdfCollider]));checked(sdfSolver.step(1.0/120.0,1));
|
||||
local sdfState=checked(sdfSolver.snapshot());
|
||||
if(sdfState.version!=14||sdfState.sdfColliders.len()!=1||sdfState.particles[0].position[0]<=0.45)
|
||||
throw "Owned SDF collider runtime/schema mismatch";
|
||||
dirty = true;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Opt-in check for the package AddRandomVelocity actor helper.
|
||||
function verifyVolumeFluidRandomVelocity() {
|
||||
local defaults = fluids.volumeDefaults();
|
||||
if (!defaults.ok) throw defaults.error.message;
|
||||
local created = fluids.newVolumeSimulator(defaults.value);
|
||||
if (!created.ok) throw created.error.message;
|
||||
local solver = created.value;
|
||||
local emission = fluids.volumeEmissionDefaults();
|
||||
if (!emission.ok) throw emission.error.message;
|
||||
local a = clone emission.value.description.prototype;
|
||||
local b = clone emission.value.description.prototype;
|
||||
a.position = [-0.2, 1.0, 0.0]; a.velocity = [1.0, 0.0, 0.0];
|
||||
b.position = [0.2, 1.0, 0.0]; b.velocity = [0.0, 1.0, 0.0];
|
||||
local admitted = solver.emit([a, b]);
|
||||
if (!admitted.ok) throw admitted.error.message;
|
||||
local applied = solver.addRandomVelocity(5.0, 73);
|
||||
if (!applied.ok) throw applied.error.message;
|
||||
local state = solver.snapshot();
|
||||
if (!state.ok) throw state.error.message;
|
||||
local da = [state.value.particles[0].velocity[0] - 1.0,
|
||||
state.value.particles[0].velocity[1],
|
||||
state.value.particles[0].velocity[2]];
|
||||
local db = [state.value.particles[1].velocity[0],
|
||||
state.value.particles[1].velocity[1] - 1.0,
|
||||
state.value.particles[1].velocity[2]];
|
||||
for (local i = 0; i < 3; ++i)
|
||||
if (fabs(da[i] - db[i]) > 0.00001) throw "Actor impulse differs per particle";
|
||||
if (solver.addRandomVelocity(-1.0, 73).ok) throw "Invalid impulse intensity accepted";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Opt-in check for Fluid3D's independent generateReflection setting.
|
||||
function verifyReflectionToggle() {
|
||||
local localSurface = fluids.newSurfaceRenderer(64, 64);
|
||||
local material = localSurface.configureMaterial(false, 0.0, 0.0, 0.0, 0.85, 1.0);
|
||||
if (!material.ok) throw material.error.message;
|
||||
local disabled = localSurface.configureReflection(false);
|
||||
if (!disabled.ok) throw disabled.error.message;
|
||||
local enabled = localSurface.configureReflection(true);
|
||||
if (!enabled.ok) throw enabled.error.message;
|
||||
return "VOLUME_REFLECTION_TOGGLE_PASS";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Opt-in check for Fluid3D's independent generateRefraction setting.
|
||||
function verifyRefractionToggle() {
|
||||
local localSurface = fluids.newSurfaceRenderer(64, 64);
|
||||
local configured = localSurface.configureRefraction(0.8, 4.0, 0.015, 2);
|
||||
if (!configured.ok) throw configured.error.message;
|
||||
local disabled = localSurface.configureRefractionEnabled(false);
|
||||
if (!disabled.ok) throw disabled.error.message;
|
||||
local enabled = localSurface.configureRefractionEnabled(true);
|
||||
if (!enabled.ok) throw enabled.error.message;
|
||||
return "VOLUME_REFRACTION_TOGGLE_PASS";
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Opt-in check for Fluid3DDistanceFieldRenderer-style SDF cutaway data.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
local collider = checked(fluids.volumeSdfColliderDefaults());
|
||||
collider.label = 93;
|
||||
collider.position = [1.0, 2.0, 3.0];
|
||||
collider.scale = 2.0;
|
||||
checked(localSolver.setSdfColliders([collider]));
|
||||
local cutaway = checked(localSolver.debugSdfSlice(93, 2, 0.5, 0.5, 65536));
|
||||
if (cutaway.width < 2 || cutaway.height < 2 ||
|
||||
cutaway.values.len() != cutaway.width * cutaway.height)
|
||||
throw "SDF slice dimensions mismatch";
|
||||
local hasInside = false;
|
||||
local hasOutside = false;
|
||||
foreach (value in cutaway.values) {
|
||||
if (value < 0.5) hasInside = true;
|
||||
if (value > 0.5) hasOutside = true;
|
||||
}
|
||||
if (!hasInside || !hasOutside) throw "SDF slice did not preserve signed regions";
|
||||
local rejected = localSolver.debugSdfSlice(93, 2, 0.5, 0.5, 1);
|
||||
if (rejected.ok) throw "SDF slice budget was not enforced";
|
||||
return format("VOLUME_SDF_SLICE_PASS %dx%d", cutaway.width, cutaway.height);
|
||||
@@ -0,0 +1,35 @@
|
||||
// Opt-in check for Fluid3DEmitter.useShapeColor across burst and moving-jet emission.
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.capacity = 16;
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
local emission = checked(fluids.volumeEmissionDefaults());
|
||||
emission.description.shape = 5;
|
||||
emission.description.direction = [0.0, 0.0, 1.0];
|
||||
emission.description.speed = 6.0;
|
||||
emission.description.prototype.color = [0.8, 0.6, 0.4, 1.0];
|
||||
emission.description.distribution = [{
|
||||
position = [0.0, 0.0, 0.0],
|
||||
color = [0.5, 0.25, 0.1, 0.5],
|
||||
direction = [0.0, 0.0, 1.0]
|
||||
}];
|
||||
|
||||
local colored = checked(fluids.newVolumeSimulator(definition));
|
||||
if (checked(colored.emitBurst(emission, 1)) != 1) throw "Shape-colored burst failed";
|
||||
local color = checked(colored.snapshot()).particles[0].color;
|
||||
if (fabs(color[0] - 0.5) > 0.00001 || fabs(color[1] - 0.25) > 0.00001 ||
|
||||
fabs(color[2] - 0.1) > 0.00001 || fabs(color[3] - 0.5) > 0.00001)
|
||||
throw "Shape color was not assigned";
|
||||
|
||||
emission.description.useShapeColor = false;
|
||||
local plain = checked(fluids.newVolumeSimulator(definition));
|
||||
local jet = eve.VolumeFluidJetEmitter();
|
||||
if (checked(jet.advance(plain, emission, 1.0 / 30.0, 4, 0.0)) < 1)
|
||||
throw "Uncolored moving jet failed";
|
||||
foreach (particle in checked(plain.snapshot()).particles) {
|
||||
color = particle.color;
|
||||
if (fabs(color[0] - 0.8) > 0.00001 || fabs(color[1] - 0.6) > 0.00001 ||
|
||||
fabs(color[2] - 0.4) > 0.00001 || fabs(color[3] - 1.0) > 0.00001)
|
||||
throw "Disabled shape color changed the prototype color";
|
||||
}
|
||||
print("VOLUME_SHAPE_COLOR_ASSIGNMENT_PASS\n");
|
||||
return "VOLUME_SHAPE_COLOR_PASS";
|
||||
@@ -0,0 +1,33 @@
|
||||
// Opt-in setup/runtime contract probe for Fluid3D sphere/cube distribution shapes.
|
||||
function runShapeDistributionCheck() {
|
||||
local sphere=checked(fluids.volumeEmissionFromSphere(0.2,0.1,true));
|
||||
local cube=checked(fluids.volumeEmissionFromCube(0.2,0.3,0.4,0.1,false));
|
||||
local edge=checked(fluids.volumeEmissionFromEdge(0.5,0.1,30.0));
|
||||
local disk=checked(fluids.volumeEmissionFromDisk(0.2,0.1,true));
|
||||
if(sphere.version!=11||cube.version!=11||sphere.description.randomVelocity!=0.0||sphere.description.distribution.len()==0||
|
||||
cube.description.distribution.len()!=60||edge.description.distribution.len()!=5||
|
||||
disk.description.distribution.len()==0)
|
||||
throw "Analytic distribution shape mismatch";
|
||||
local point=sphere.description.distribution[0];
|
||||
if(point.direction.len()!=3)throw "Distribution direction missing";
|
||||
local definition=checked(fluids.volumeDefaults());definition.settings.gravity=[0.0,0.0,0.0];
|
||||
local testSolver=checked(fluids.newVolumeSimulator(definition));
|
||||
sphere.description.origin=[0.0,1.0,0.0];sphere.description.speed=2.0;
|
||||
if(checked(testSolver.emitBurst(sphere,1))!=1||testSolver.getParticleCount()!=1)
|
||||
throw "Sphere distribution burst failed";
|
||||
if(edge.description.distribution[3].direction[1]>-0.999||
|
||||
fabs(disk.description.distribution[0].position[0])>0.201)
|
||||
throw "Edge radial velocity or disk edge emission mismatch";
|
||||
local randomNozzle=checked(fluids.volumeEmissionDefaults());randomNozzle.description.origin=[0.2,1.0,0.0];
|
||||
randomNozzle.description.direction=[0.0,0.0,1.0];randomNozzle.description.speed=2.0;
|
||||
randomNozzle.description.randomVelocity=1.0;randomNozzle.description.seed=77;
|
||||
if(checked(testSolver.emitBurst(randomNozzle,1))!=1)throw "Random velocity burst failed";
|
||||
local randomParticle=checked(testSolver.snapshot()).particles[1];
|
||||
local speed=sqrt(randomParticle.velocity[0]*randomParticle.velocity[0]+randomParticle.velocity[1]*randomParticle.velocity[1]+randomParticle.velocity[2]*randomParticle.velocity[2]);
|
||||
if(fabs(speed-2.0)>0.0001||randomParticle.velocity[2]>1.999)throw "Fluid3D randomVelocity blend mismatch";
|
||||
print("VOLUME_SHAPE_DISTRIBUTION_PASS sphere="+sphere.description.distribution.len()+
|
||||
" cube="+cube.description.distribution.len()+" edge="+edge.description.distribution.len()+
|
||||
" disk="+disk.description.distribution.len()+"\n");
|
||||
}
|
||||
|
||||
runShapeDistributionCheck();
|
||||
@@ -0,0 +1,19 @@
|
||||
// Opt-in SimpleFluid numerical acceptance probe. Leaves the final surface paused.
|
||||
function verifySimpleFluid() {
|
||||
paused = true;
|
||||
rebuild(0);
|
||||
local initial = checked(solver.snapshot()).particles;
|
||||
for (local frame=0; frame<120; ++frame) checked(solver.step(1.0/60.0, 2));
|
||||
local current = checked(solver.snapshot()).particles;
|
||||
local moved=0, minimumY=1000.0, maximumY=-1000.0;
|
||||
for (local i=0; i<current.len(); ++i) {
|
||||
if (fabs(current[i].position[1]-initial[i].position[1])>0.05) ++moved;
|
||||
if (current[i].position[1]<minimumY) minimumY=current[i].position[1];
|
||||
if (current[i].position[1]>maximumY) maximumY=current[i].position[1];
|
||||
}
|
||||
if (current.len()!=640 || moved<500 || minimumY<0.0 || maximumY>2.4)
|
||||
throw "SimpleFluid numerical acceptance failed";
|
||||
dirty=true;
|
||||
return "VOLUME_SIMPLE_FLUID_PASS particles="+current.len()+" moved="+moved+
|
||||
" minY="+minimumY+" maxY="+maximumY;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Opt-in bounded stability soak. Drive volumeFluidSoakChunk() from MCP in short calls.
|
||||
persist volumeFluidSoakFrames = 0;
|
||||
|
||||
function beginVolumeFluidSoak() {
|
||||
paused = true;
|
||||
rebuild(0);
|
||||
volumeFluidSoakFrames = 0;
|
||||
dirty = false;
|
||||
if (solver.getParticleCount() != 640)
|
||||
throw "volume fluid soak requires the 640-particle SimpleFluid scene";
|
||||
return "VOLUME_FLUID_SOAK_READY particles=640";
|
||||
}
|
||||
|
||||
function volumeFluidSoakChunk(frameCount) {
|
||||
if (frameCount < 1 || frameCount > 10)
|
||||
throw "volume fluid soak chunk must contain 1..10 frames";
|
||||
for (local frame = 0; frame < frameCount; ++frame) {
|
||||
checked(solver.step(1.0 / 120.0, 1));
|
||||
// Periodically cover the host readback/upload path. Other frames exercise
|
||||
// the device-local uniform-color path without creating per-frame resources.
|
||||
if ((volumeFluidSoakFrames % 10) == 0) {
|
||||
surface.renderVolume(solver);
|
||||
checked(surface.copyToTexture(gfx, texture));
|
||||
} else {
|
||||
checked(surface.renderVolumeColorToTexture(solver, gfx, texture));
|
||||
}
|
||||
++volumeFluidSoakFrames;
|
||||
}
|
||||
if (solver.getParticleCount() != 640)
|
||||
throw "volume fluid soak particle count changed";
|
||||
return "VOLUME_FLUID_SOAK_CHUNK frames=" + volumeFluidSoakFrames + " particles=640";
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0]; definition.settings.capacity = 2;
|
||||
local prototype = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
local a = clone prototype; a.material = clone prototype.material;
|
||||
a.position = [-0.2, 1.0, 0.0]; a.material.density = 1000.0; a.selfCollide = false;
|
||||
local b = clone prototype; b.material = clone prototype.material;
|
||||
b.position = [0.2, 1.0, 0.0]; b.material.density = 2000.0; b.selfCollide = false;
|
||||
definition.particles = [a, b];
|
||||
local localSolver = checked(fluids.newVolumeSimulator(definition));
|
||||
checked(localSolver.setStitches([{particleIndex1=0,particleIndex2=1,compliance=0.0}]));
|
||||
checked(localSolver.step(1.0 / 120.0, 1));
|
||||
local state = checked(localSolver.snapshot());
|
||||
if (state.version != 20 || state.stitches.len() != 1 ||
|
||||
fabs(state.particles[0].position[0] - state.particles[1].position[0]) > 0.00001 ||
|
||||
fabs(state.particles[0].position[0] - 0.06666667) > 0.00001)
|
||||
throw format("Stitch mass weighting mismatch: %.7f %.7f", state.particles[0].position[0],
|
||||
state.particles[1].position[0]);
|
||||
checked(localSolver.killParticle(0));
|
||||
if (checked(localSolver.snapshot()).stitches.len() != 0) throw "Dead-particle stitch survived";
|
||||
return "VOLUME_STITCH_PASS";
|
||||
@@ -0,0 +1,9 @@
|
||||
local localSurface = fluids.newSurfaceRenderer(17, 17);
|
||||
local configured = localSurface.configureSurfaceBlend(5, 10);
|
||||
if (!configured.ok) throw "default surface blend rejected";
|
||||
if (localSurface.configureSurfaceBlend(-1, 10).ok) throw "negative blend factor accepted";
|
||||
if (localSurface.configureSurfaceBlend(5, 11).ok) throw "out-of-range blend factor accepted";
|
||||
if (!localSurface.configureParticleBlend(2, 0, false).ok) throw "particle blend rejected";
|
||||
if (localSurface.configureParticleBlend(11, 0, false).ok) throw "particle source factor accepted";
|
||||
if (localSurface.configureParticleBlend(2, -1, true).ok) throw "particle destination factor accepted";
|
||||
::surfaceBlendSettingsPass <- true;
|
||||
@@ -0,0 +1,11 @@
|
||||
// Opt-in check for Fluid3D's world-space surface blur radius.
|
||||
function verifySurfaceBlurRadius() {
|
||||
local localSurface = fluids.newSurfaceRenderer(64, 64);
|
||||
local configured = localSurface.configureSurfaceBlurRadius(0.02);
|
||||
if (!configured.ok) throw configured.error.message;
|
||||
if (localSurface.configureSurfaceBlurRadius(-0.01).ok)
|
||||
throw "Negative surface blur radius was accepted";
|
||||
if (localSurface.configureSurfaceBlurRadius(0.1001).ok)
|
||||
throw "Oversized surface blur radius was accepted";
|
||||
return "VOLUME_SURFACE_BLUR_PASS";
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Opt-in check for Fluid3D's generateSurface=false all-phase volume presentation.
|
||||
function verifySurfaceDisabledVolume() {
|
||||
local defaults = fluids.volumeDefaults();
|
||||
if (!defaults.ok) throw defaults.error.message;
|
||||
local created = fluids.newVolumeSimulator(defaults.value);
|
||||
if (!created.ok) throw created.error.message;
|
||||
local localSolver = created.value;
|
||||
local emission = fluids.volumeEmissionDefaults();
|
||||
if (!emission.ok) throw emission.error.message;
|
||||
local particle = clone emission.value.description.prototype;
|
||||
particle.position = [0.0, 1.0, 0.0];
|
||||
particle.color = [0.9, 0.1, 0.05, 1.0];
|
||||
particle.material.phase = 0; // VolumeFluidPhase::Liquid
|
||||
local emitted = localSolver.emit([particle]);
|
||||
if (!emitted.ok) throw emitted.error.message;
|
||||
local localSurface = fluids.newSurfaceRenderer(64, 64);
|
||||
localSurface.setCameraXYZ(0.0, 1.0, 3.0, 0.0, 1.0, 0.0, 45.0);
|
||||
local rendererSettings = checked(fluids.fluidRendererSettingsDefaults());
|
||||
if (rendererSettings.blendSource != 5 || rendererSettings.blendDestination != 10 ||
|
||||
rendererSettings.thicknessCutoff != 1.2 || rendererSettings.thicknessDownsample != 2 ||
|
||||
rendererSettings.blurRadius != 0.02)
|
||||
throw "Fluid3D renderer defaults mismatch";
|
||||
rendererSettings.generateSurface = false;
|
||||
rendererSettings.generateReflection = false;
|
||||
rendererSettings.generateRefraction = false;
|
||||
rendererSettings.generateFoam = false;
|
||||
checked(localSurface.configureRendererSettings(rendererSettings));
|
||||
if (checked(localSurface.rendererSettings()).generateSurface)
|
||||
throw "Complete renderer settings were not applied";
|
||||
local rendered = localSurface.renderConfiguredVolume(localSolver);
|
||||
if (!rendered.ok) throw rendered.error.message;
|
||||
checked(localSurface.configureSurfaceEnabled(true));
|
||||
checked(localSurface.renderConfiguredVolume(localSolver));
|
||||
checked(localSurface.configureSurfaceEnabled(false));
|
||||
if (localSurface.renderVolumeWithoutSurface(localSolver, 0.0).ok)
|
||||
throw "Invalid surface-disabled absorption was accepted";
|
||||
return "VOLUME_SURFACE_DISABLED_PASS";
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Opt-in real-renderer check for Fluid3D renderablePositions timing.
|
||||
function verifyVolumeFluidSurfaceInterpolation() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
local particle = clone checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [-0.5, 1.0, 0.0];
|
||||
particle.velocity = [60.0, 0.0, 0.0];
|
||||
definition.particles.append(particle);
|
||||
local source = checked(fluids.newVolumeSimulator(definition));
|
||||
checked(source.step(1.0 / 120.0, 1));
|
||||
local renderer = surface;
|
||||
checked(renderer.configureProjection(true, 1.0));
|
||||
renderer.setCameraXYZ(0.0, 1.0, 3.0, 0.0, 1.0, 0.0, 48.0);
|
||||
checked(renderer.prepare());
|
||||
checked(renderer.renderVolumeInterpolated(source, 0.0));
|
||||
checked(renderer.renderVolumeInterpolated(source, 1.0));
|
||||
if (renderer.renderVolumeInterpolated(source, -0.01).ok)
|
||||
throw "Invalid surface interpolation accepted";
|
||||
checked(renderer.renderVolumeInterpolated(source, 0.0));
|
||||
checked(renderer.copyToImage(pixels));
|
||||
gfx.updateTextureFromImageData(texture, pixels);
|
||||
paused = true;
|
||||
dirty = false;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Opt-in check for the package Fluid3DActorTeleport behavior.
|
||||
function verifyVolumeFluidTeleport() {
|
||||
local defaults = fluids.volumeDefaults();
|
||||
if (!defaults.ok) throw defaults.error.message;
|
||||
local created = fluids.newVolumeSimulator(defaults.value);
|
||||
if (!created.ok) throw created.error.message;
|
||||
local solver = created.value;
|
||||
local emission = fluids.volumeEmissionDefaults();
|
||||
if (!emission.ok) throw emission.error.message;
|
||||
local particle = clone emission.value.description.prototype;
|
||||
particle.position = [0.0, 1.0, 0.0];
|
||||
particle.velocity = [1.0, 2.0, 3.0];
|
||||
local admitted = solver.emit([particle]);
|
||||
if (!admitted.ok) throw admitted.error.message;
|
||||
local current = {position=[0.0,1.0,0.0], rotation=[0.0,0.0,0.0,1.0]};
|
||||
local target = {position=[1.0,1.0,0.0], rotation=[0.0,0.0,0.0,1.0]};
|
||||
local moved = solver.teleportActor(current, target);
|
||||
if (!moved.ok) throw moved.error.message;
|
||||
local state = solver.snapshot();
|
||||
if (!state.ok) throw state.error.message;
|
||||
local result = state.value.particles[0];
|
||||
if (fabs(result.position[0]-1.0)>0.00001 || fabs(result.velocity[0])>0.00001 ||
|
||||
fabs(result.velocity[1])>0.00001 || fabs(result.velocity[2])>0.00001)
|
||||
throw "Actor teleport state mismatch";
|
||||
target.position = [4.0, 1.0, 0.0];
|
||||
if (solver.teleportActor(current, target).ok) throw "Out-of-bounds teleport accepted";
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Opt-in one-step contact heating check; no render calls or callbacks.
|
||||
function verifyVolumeFluidThermalContact() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
local particle = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.0, 1.25, 0.0];
|
||||
particle.data = [5.0, 1.0, 7.0, -2.0];
|
||||
definition.particles = [particle];
|
||||
local sim = checked(fluids.newVolumeSimulator(definition));
|
||||
local collider = checked(fluids.volumeColliderDefaults());
|
||||
collider.label = 17;
|
||||
checked(sim.setColliders([collider]));
|
||||
local rule = checked(fluids.volumeThermalRuleDefaults());
|
||||
rule.colliderLabel = 17;
|
||||
rule.rate = -6.0;
|
||||
checked(sim.stepWithThermalContacts(1.0 / 60.0, 4, [rule]));
|
||||
local state = checked(sim.snapshot()).particles[0];
|
||||
if (fabs(state.data[0] - 4.9) > 0.00001 || fabs(state.data[1] - 0.9) > 0.00001)
|
||||
throw "Contact heating did not apply exactly once";
|
||||
checked(sim.applyMaterialChannels());
|
||||
if (fabs(checked(sim.snapshot()).particles[0].material.viscosity - 4.9) > 0.00001)
|
||||
throw "Heated channel did not drive viscosity";
|
||||
if (sim.stepWithThermalContacts(1.0 / 60.0, 4, [rule, rule]).ok)
|
||||
throw "Duplicate thermal labels accepted";
|
||||
rule.unknown <- 1;
|
||||
if (sim.stepWithThermalContacts(1.0 / 60.0, 4, [rule]).ok)
|
||||
throw "Unknown thermal rule field accepted";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Heat and rigid feedback share one simulation step; world integration stays caller-owned.
|
||||
function verifyVolumeFluidThermalCoupling() {
|
||||
local world = physics.newWorld3D(0.0, 0.0, 0.0, false);
|
||||
try {
|
||||
local body = world.newBody("dynamic", 0.0, 0.0, 0.0);
|
||||
body.newBoxShape(0.5, 0.5, 0.5, 1000.0, 0.5, 0.0);
|
||||
local bridge = eve.VolumeFluidCoupling();
|
||||
local collider = checked(fluids.volumeColliderDefaults());
|
||||
collider.label = 17;
|
||||
checked(bridge.attach(body, collider));
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
local particle = checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.0, 1.25, 0.0];
|
||||
particle.data = [5.0, 1.0, 7.0, -2.0];
|
||||
particle.life = 1.0;
|
||||
definition.particles = [particle];
|
||||
local sim = checked(fluids.newVolumeSimulator(definition));
|
||||
local rule = checked(fluids.volumeThermalRuleDefaults());
|
||||
rule.colliderLabel = 99;
|
||||
rule.rate = -6.0;
|
||||
if (bridge.stepWithThermalContacts(world, sim, 1.0/60.0, 4, [rule]).ok)
|
||||
throw "Unlinked thermal label accepted";
|
||||
local before = checked(sim.snapshot());
|
||||
if (before.colliders.len() != 0 || before.particles[0].life != 1.0 || body.getLinearVelocityY() != 0.0)
|
||||
throw "Rejected coupling mutated state";
|
||||
rule.colliderLabel = 17;
|
||||
local contacts = checked(bridge.stepWithThermalContacts(world, sim, 1.0/60.0, 4, [rule]));
|
||||
local after = checked(sim.snapshot()).particles[0];
|
||||
if (contacts <= 0 || fabs(body.getLinearVelocityY()) < 0.000001)
|
||||
throw "Coupled thermal step omitted rigid feedback";
|
||||
if (fabs(after.data[0]-4.9) > 0.00001 || fabs(after.life-(1.0-1.0/60.0)) > 0.000001)
|
||||
throw "Heat and fluid did not share exactly one step";
|
||||
world.destroy();
|
||||
if (bridge.stepWithThermalContacts(world, sim, 1.0/60.0, 4, [rule]).ok)
|
||||
throw "Destroyed world accepted";
|
||||
} catch (error) {
|
||||
world.destroy();
|
||||
throw error;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Small, bounded thermal presentation check; no render call inside the loop.
|
||||
function verifyVolumeFluidThermalColors() {
|
||||
rebuild(10);
|
||||
for (local i = 0; i < 120; ++i) {
|
||||
checked(solver.stepWithThermalContacts(1.0/60.0, 4, thermalRules));
|
||||
checked(solver.applyMaterialChannelsWithColors(viscosityColors));
|
||||
}
|
||||
local particles = checked(solver.snapshot()).particles;
|
||||
if (particles.len() != 96) throw "Thermal scene count changed";
|
||||
local hot = 0.0; local cold = 0.0;
|
||||
local hotBlue = 0.0; local coldBlue = 0.0;
|
||||
for (local i = 0; i < particles.len(); ++i) {
|
||||
local p = particles[i];
|
||||
if (p.material.viscosity != p.data[0]) throw "Thermal material mapping missing";
|
||||
if (i < 48) { hot += p.material.viscosity; hotBlue += p.color[2]; }
|
||||
else { cold += p.material.viscosity; coldBlue += p.color[2]; }
|
||||
}
|
||||
if (!(hot / 48.0 < 4.9 && cold / 48.0 > 5.1 && coldBlue > hotBlue))
|
||||
throw "Hot and cold material colors did not diverge";
|
||||
paused = true; dirty = true;
|
||||
return [hot / 48.0, cold / 48.0];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Opt-in contract check for Fluid3D's normalized ThicknessClip convention.
|
||||
local localSurface = fluids.newSurfaceRenderer(32, 32);
|
||||
local configured = localSurface.configureSurface(1.0, 1.2, 0.03, 2);
|
||||
if (!configured.ok) throw "Fluid3D thickness cutoff was rejected";
|
||||
if (localSurface.configureSurface(1.0, -0.001, 0.03, 2).ok)
|
||||
throw "Negative thickness cutoff was accepted";
|
||||
if (localSurface.configureSurface(1.0, 5.001, 0.03, 2).ok)
|
||||
throw "Oversized thickness cutoff was accepted";
|
||||
::thicknessCutoffSettingsPass <- true;
|
||||
@@ -0,0 +1,53 @@
|
||||
// Opt-in FluidViscosity acceptance probe. Loading this file has no side effects.
|
||||
function viscosityScene(viscosity) {
|
||||
local definition=checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity=[0.0,0.0,0.0];
|
||||
definition.settings.spacing=0.1;
|
||||
definition.settings.minimum=[-0.5,0.0,-0.5];
|
||||
definition.settings.maximum=[0.5,1.2,0.5];
|
||||
definition.settings.iterations=2;
|
||||
local prototype=checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
for(local z=0;z<4;++z)for(local y=0;y<4;++y)for(local x=0;x<4;++x) {
|
||||
local particle=clone prototype;
|
||||
particle.material=clone prototype.material;
|
||||
particle.position=[-0.15+x*0.1,0.45+y*0.1,-0.15+z*0.1];
|
||||
particle.velocity=[0.0,0.0,y<2 ? 0.5 : -0.5];
|
||||
particle.material.viscosity=viscosity;
|
||||
particle.material.yieldStress=0.0;
|
||||
particle.material.cohesion=0.0;
|
||||
definition.particles.append(particle);
|
||||
}
|
||||
return checked(fluids.newVolumeSimulator(definition));
|
||||
}
|
||||
|
||||
function relativeFlowEnergy(particles) {
|
||||
local mean=[0.0,0.0,0.0];
|
||||
foreach(particle in particles)for(local c=0;c<3;++c)mean[c]+=particle.velocity[c];
|
||||
for(local c=0;c<3;++c)mean[c]/=particles.len();
|
||||
local energy=0.0;
|
||||
foreach(particle in particles)for(local c=0;c<3;++c) {
|
||||
local relative=particle.velocity[c]-mean[c];
|
||||
energy+=relative*relative;
|
||||
}
|
||||
return energy/particles.len();
|
||||
}
|
||||
|
||||
function verifyVolumeFluidViscosity() {
|
||||
local low=viscosityScene(0.0),high=viscosityScene(70.0);
|
||||
local initialLow=relativeFlowEnergy(checked(low.snapshot()).particles);
|
||||
local initialHigh=relativeFlowEnergy(checked(high.snapshot()).particles);
|
||||
for(local frame=0;frame<120;++frame) {
|
||||
checked(low.step(1.0/120.0,1));
|
||||
checked(high.step(1.0/120.0,1));
|
||||
}
|
||||
local finalLow=relativeFlowEnergy(checked(low.snapshot()).particles);
|
||||
local finalHigh=relativeFlowEnergy(checked(high.snapshot()).particles);
|
||||
if(fabs(initialLow-initialHigh)>0.000001)
|
||||
throw "Viscosity controls did not start identically";
|
||||
if(finalHigh>=finalLow*0.5)
|
||||
throw "High viscosity did not materially damp relative flow";
|
||||
if(finalLow<=initialLow*0.25)
|
||||
throw "Low-viscosity control lost most relative flow";
|
||||
return "VOLUME_FLUID_VISCOSITY_PASS initial="+initialLow+
|
||||
" low="+finalLow+" high="+finalHigh+" ratio="+(finalHigh/finalLow);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
dofile("wave-support.nut");
|
||||
|
||||
local original = [1.0, 2.0, 3.0];
|
||||
local quarter = fluidWavePosition(original, 0.5, 2.0, 3.14159265359 / 4.0);
|
||||
if (fabs(quarter[0] - 1.5) > 0.00001 || quarter[1] != 2.0 || quarter[2] != 3.0)
|
||||
throw "WaveGenerator position mismatch";
|
||||
local replay = fluidWavePosition(original, 0.5, 2.0, 3.14159265359 / 4.0);
|
||||
if (replay[0] != quarter[0] || replay[1] != quarter[1] || replay[2] != quarter[2])
|
||||
throw "WaveGenerator replay mismatch";
|
||||
rebuild(11);
|
||||
advanceAttachment(3.14159265359 / 2.0);
|
||||
local integrated = checked(solver.snapshot());
|
||||
if (fabs(integrated.colliders[0].center[0] - 0.3) > 0.00001)
|
||||
throw "WaveGenerator moving collider integration mismatch";
|
||||
return "VOLUME_WAVE_GENERATOR_PASS";
|
||||
@@ -0,0 +1,6 @@
|
||||
// Deterministic native equivalent of the package WaveGenerator sample.
|
||||
// fixedTime is injected by the simulation owner, so pause/replay never observes wall time.
|
||||
function fluidWavePosition(originalPosition, amplitude, frequency, fixedTime) {
|
||||
return [originalPosition[0] + sin(fixedTime * frequency) * amplitude,
|
||||
originalPosition[1], originalPosition[2]];
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Load from the paused volume-fluid example with dofile("wheel-check.nut").
|
||||
// This defines an opt-in bounded check; loading alone performs no simulation.
|
||||
function verifyVolumeFluidWheel() {
|
||||
paused = true;
|
||||
local run = function(offset, ticks, emitWater) {
|
||||
rebuild(8);
|
||||
nozzle.description.origin[0] = offset;
|
||||
for (local i = 0; i < ticks; ++i) {
|
||||
wheelContacts += checked(wheelCoupling.step(wheelWorld, solver, 1.0 / 60.0, 4));
|
||||
wheelWorld.update(1.0 / 60.0);
|
||||
if (emitWater) advanceWheelJet(1.0 / 60.0);
|
||||
}
|
||||
return { speed = wheelBody.getAngularVelocityZ(), contacts = wheelContacts,
|
||||
particles = solver.getParticleCount() };
|
||||
};
|
||||
try {
|
||||
local dry = run(0.25, 60, false);
|
||||
if (!(fabs(dry.speed) <= 0.00001) || dry.contacts != 0 || dry.particles != 0)
|
||||
throw "Dry wheel moved or produced fluid contacts";
|
||||
local right = run(0.25, 240, true);
|
||||
if (!(right.speed < -0.001 && right.speed > -100.0) || right.contacts == 0)
|
||||
throw "Right jet did not drive a finite clockwise wheel motion";
|
||||
local left = run(-0.25, 240, true);
|
||||
if (!(left.speed > 0.001 && left.speed < 100.0) || left.contacts == 0)
|
||||
throw "Left jet did not reverse wheel motion";
|
||||
dirty = true;
|
||||
return { dry = dry, right = right, left = left };
|
||||
} catch (error) {
|
||||
failure = error.tostring();
|
||||
dirty = true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Opt-in WhiskeyBottle acceptance. Loading this file is inert.
|
||||
function verifyVolumeFluidWhiskeyBottle() {
|
||||
paused=true;rebuild(17);
|
||||
local initial=checked(solver.snapshot());
|
||||
if(initial.sdfColliders.len()!=1 || initial.sdfColliders[0].sdf.distances.len()==0)
|
||||
throw "Bottle model SDF was not baked";
|
||||
local sampleCount=initial.sdfColliders[0].sdf.distances.len();
|
||||
local sampleFirst=initial.sdfColliders[0].sdf.distances[0];
|
||||
local uprightOutside=0;local peakContacts=0;
|
||||
for(local frame=0;frame<150;++frame) {
|
||||
checked(solver.step(1.0/120.0,1));
|
||||
if(frame%30==29) {local contacts=checked(solver.contacts()).len();if(contacts>peakContacts)peakContacts=contacts;}
|
||||
}
|
||||
foreach(particle in checked(solver.snapshot()).particles)
|
||||
if(fluidBottleOutsideCavity(particle,0.0))++uprightOutside;
|
||||
for(local frame=0;frame<300;++frame) {
|
||||
local next=bottle.angle+0.007;if(next>2.05)next=2.05;
|
||||
local velocity=(next-bottle.angle)*120.0;bottle.angle=next;
|
||||
bottle.pose=fluidBottlePose(bottle.angle,velocity);
|
||||
checked(solver.updateSdfColliderPoses([bottle.pose]));
|
||||
checked(solver.step(1.0/120.0,1));
|
||||
if(frame%30==29) {local contacts=checked(solver.contacts()).len();if(contacts>peakContacts)peakContacts=contacts;}
|
||||
}
|
||||
for(local frame=0;frame<300;++frame) {
|
||||
checked(solver.step(1.0/120.0,1));
|
||||
if(frame%30==29) {local contacts=checked(solver.contacts()).len();if(contacts>peakContacts)peakContacts=contacts;}
|
||||
}
|
||||
local poured=0;foreach(particle in checked(solver.snapshot()).particles)
|
||||
if(fluidBottleOutsideCavity(particle,bottle.angle))++poured;
|
||||
local after=checked(solver.snapshot());
|
||||
if(after.sdfColliders[0].sdf.distances.len()!=sampleCount ||
|
||||
after.sdfColliders[0].sdf.distances[0]!=sampleFirst)
|
||||
throw "Bottle pose update rebuilt or changed SDF samples";
|
||||
if(uprightOutside!=0 || peakContacts<=0 || poured<=0)
|
||||
throw "Bottle containment/pour mismatch upright="+uprightOutside+" contacts="+peakContacts+" poured="+poured;
|
||||
dirty=true;
|
||||
return "VOLUME_FLUID_WHISKEY_BOTTLE_PASS particles="+solver.getParticleCount()+
|
||||
" samples="+sampleCount+" uprightOutside="+uprightOutside+
|
||||
" contacts="+peakContacts+" poured="+poured;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Opt-in check for the package WorldSpaceGravity component behavior.
|
||||
function verifyVolumeFluidWorldGravity() {
|
||||
local definition = checked(fluids.volumeDefaults());
|
||||
definition.settings.gravity = [0.0, 0.0, 0.0];
|
||||
local particle = clone checked(fluids.volumeEmissionDefaults()).description.prototype;
|
||||
particle.position = [0.0, 1.0, 0.0];
|
||||
definition.particles.append(particle);
|
||||
local source = checked(fluids.newVolumeSimulator(definition));
|
||||
checked(source.setGravity(0.0, 9.81, 0.0));
|
||||
checked(source.step(1.0 / 120.0, 1));
|
||||
local state = checked(source.snapshot());
|
||||
if (state.particles[0].velocity[1] <= 0.0 || state.settings.gravity[1] != 9.81)
|
||||
throw "Dynamic world gravity was not applied";
|
||||
if (source.setGravity(1001.0, 0.0, 0.0).ok)
|
||||
throw "Out-of-range gravity accepted";
|
||||
local unchanged = checked(source.snapshot());
|
||||
if (unchanged.settings.gravity[1] != 9.81)
|
||||
throw "Invalid gravity changed solver settings";
|
||||
return true;
|
||||
}
|
||||
@@ -135,7 +135,7 @@ void main() {
|
||||
dens.d[i] = rho;
|
||||
grad.g[i] = vec4(gradSum, 0.0);
|
||||
float rho0 = max(pc.d[4], 1e-6);
|
||||
float C = rho / rho0 - 1.0;
|
||||
float C = max(0.0, rho / rho0 - 1.0);
|
||||
lam.l[i] = -C / (dot(gradSum, gradSum) + 1e-6);
|
||||
}
|
||||
)GLSL";
|
||||
@@ -199,10 +199,11 @@ inline const char* kFluidApplyDelta = R"GLSL(
|
||||
#version 450
|
||||
layout(local_size_x = 64) in;
|
||||
layout(set = 0, binding = 0) buffer Pos { vec4 p[]; } pos;
|
||||
layout(set = 0, binding = 1) buffer Vel { vec4 v[]; } vel;
|
||||
layout(set = 0, binding = 2) buffer CellHead { int h[]; } head;
|
||||
layout(set = 0, binding = 3) buffer CellNext { int nx[]; } next;
|
||||
layout(set = 0, binding = 7) buffer Grad { vec4 g[]; } grad;
|
||||
layout(set = 0, binding = 5) buffer Sdf { float d[]; } sdf;
|
||||
layout(set = 0, binding = 7) buffer Grad { vec4 g[]; } grad;
|
||||
layout(push_constant) uniform PC { float d[32]; } pc;
|
||||
|
||||
float sdfValue(ivec3 res, int x, int y, int z) {
|
||||
@@ -216,6 +217,7 @@ float sdfSample(vec3 p) {
|
||||
float cs = pc.d[27];
|
||||
vec3 f = (p - o) / cs;
|
||||
vec3 clamped = clamp(f, vec3(0.0), vec3(res - 1));
|
||||
float outside = length((f - clamped) * cs);
|
||||
ivec3 i0 = ivec3(floor(clamped));
|
||||
ivec3 i1 = min(i0 + ivec3(1), res - ivec3(1));
|
||||
vec3 t = fract(clamped);
|
||||
@@ -233,7 +235,7 @@ float sdfSample(vec3 p) {
|
||||
float c11 = v011 + (v111 - v011) * t.x;
|
||||
float c0 = c00 + (c10 - c00) * t.y;
|
||||
float c1 = c01 + (c11 - c01) * t.y;
|
||||
return c0 + (c1 - c0) * t.z;
|
||||
return c0 + (c1 - c0) * t.z + outside;
|
||||
}
|
||||
|
||||
vec3 sdfGrad(vec3 p) {
|
||||
@@ -247,14 +249,29 @@ void main() {
|
||||
uint i = gl_GlobalInvocationID.x;
|
||||
uint n = uint(pc.d[0]);
|
||||
if (i >= n) return;
|
||||
if (pc.d[29] > 0.5) {
|
||||
vec3 v = grad.g[i].xyz;
|
||||
vec3 p = pos.p[i].xyz + v * pc.d[1];
|
||||
float radius = pc.d[2];
|
||||
float d = sdfSample(p);
|
||||
vec3 nn = sdfGrad(p);
|
||||
float nl = length(nn);
|
||||
if (nl > 1e-6) nn /= nl;
|
||||
else nn = vec3(0.0, 1.0, 0.0);
|
||||
p -= nn * (d - radius);
|
||||
v -= nn * dot(v, nn);
|
||||
pos.p[i] = vec4(p, 0.0);
|
||||
vel.v[i] = vec4(v, 0.0);
|
||||
return;
|
||||
}
|
||||
vec3 delta = grad.g[i].xyz;
|
||||
float maxD = 0.05 * pc.d[3];
|
||||
float maxD = 0.0005 * pc.d[3];
|
||||
float dl = length(delta);
|
||||
if (dl > maxD && dl > 1e-9) delta *= maxD / dl;
|
||||
vec3 p = pos.p[i].xyz + delta;
|
||||
float radius = pc.d[2];
|
||||
float d = sdfSample(p);
|
||||
if (d < radius) {
|
||||
{
|
||||
vec3 nn = sdfGrad(p);
|
||||
float nl = length(nn);
|
||||
if (nl > 1e-6) nn /= nl;
|
||||
@@ -274,6 +291,7 @@ layout(set = 0, binding = 1) buffer Vel { vec4 v[]; } vel;
|
||||
layout(set = 0, binding = 2) buffer CellHead { int h[]; } head;
|
||||
layout(set = 0, binding = 3) buffer CellNext { int nx[]; } next;
|
||||
layout(set = 0, binding = 5) buffer Sdf { float d[]; } sdf;
|
||||
layout(set = 0, binding = 7) buffer Grad { vec4 g[]; } grad;
|
||||
layout(push_constant) uniform PC { float d[32]; } pc;
|
||||
|
||||
float poly6(float r2, float h) {
|
||||
@@ -300,6 +318,7 @@ float sdfSample(vec3 p) {
|
||||
float cs = pc.d[27];
|
||||
vec3 f = (p - o) / cs;
|
||||
vec3 clamped = clamp(f, vec3(0.0), vec3(res - 1));
|
||||
float outside = length((f - clamped) * cs);
|
||||
ivec3 i0 = ivec3(floor(clamped));
|
||||
ivec3 i1 = min(i0 + ivec3(1), res - ivec3(1));
|
||||
vec3 t = fract(clamped);
|
||||
@@ -317,7 +336,7 @@ float sdfSample(vec3 p) {
|
||||
float c11 = v011 + (v111 - v011) * t.x;
|
||||
float c0 = c00 + (c10 - c00) * t.y;
|
||||
float c1 = c01 + (c11 - c01) * t.y;
|
||||
return c0 + (c1 - c0) * t.z;
|
||||
return c0 + (c1 - c0) * t.z + outside;
|
||||
}
|
||||
|
||||
vec3 sdfGrad(vec3 p) {
|
||||
@@ -344,6 +363,9 @@ void main() {
|
||||
vec3 viscAcc = vec3(0.0);
|
||||
vec3 cohAcc = vec3(0.0);
|
||||
float shearAcc = 0.0;
|
||||
float neighborWeight = 0.0;
|
||||
float cohesionWeight = 0.0;
|
||||
float particleVolume = pow(2.0 * pc.d[2], 3.0);
|
||||
vec3 origin = vec3(pc.d[17], pc.d[18], pc.d[19]);
|
||||
float cs = pc.d[20];
|
||||
ivec3 dim = ivec3(int(pc.d[14]), int(pc.d[15]), int(pc.d[16]));
|
||||
@@ -360,11 +382,15 @@ void main() {
|
||||
vec3 dx = p - pos.p[j].xyz;
|
||||
float r2 = dot(dx, dx);
|
||||
if (r2 < h2) {
|
||||
if (visc > 0.0) viscAcc += (vel.v[j].xyz - v) * poly6(r2, h);
|
||||
if (yield > 0.0) shearAcc += length(vel.v[j].xyz - v) * poly6(r2, h);
|
||||
float weight = poly6(r2, h) * particleVolume;
|
||||
if (visc > 0.0 || yield > 0.0) {
|
||||
viscAcc += (vel.v[j].xyz - v) * weight;
|
||||
neighborWeight += weight;
|
||||
}
|
||||
if (yield > 0.0) shearAcc += length(vel.v[j].xyz - v) * weight;
|
||||
if (coh > 0.0) {
|
||||
float r = sqrt(r2);
|
||||
cohAcc += -coh * cohesionKernel(r, h) * (dx / r);
|
||||
cohAcc -= dx * weight;
|
||||
cohesionWeight += weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,34 +399,28 @@ void main() {
|
||||
}
|
||||
float effVisc = visc;
|
||||
if (yield > 0.0 && shearAcc > 1e-6) effVisc += yield / shearAcc;
|
||||
v += viscAcc * (effVisc * dt);
|
||||
v += cohAcc * dt;
|
||||
if (neighborWeight > 1e-6)
|
||||
v += (viscAcc / neighborWeight) * clamp(effVisc * dt, 0.0, 1.0);
|
||||
vec3 surfaceNormal = sdfGrad(p);
|
||||
if (length(surfaceNormal) > 1e-6) surfaceNormal = normalize(surfaceNormal);
|
||||
else surfaceNormal = vec3(0.0, 1.0, 0.0);
|
||||
cohAcc -= surfaceNormal * dot(cohAcc, surfaceNormal);
|
||||
vec3 cohesionDv = cohesionWeight > 1e-6 ?
|
||||
(cohAcc / cohesionWeight) * (coh * 100.0 * dt) : vec3(0.0);
|
||||
float cohesionSpeed = length(cohesionDv);
|
||||
float maxCohesionDv = pc.d[2];
|
||||
if (cohesionSpeed > maxCohesionDv && cohesionSpeed > 1e-9)
|
||||
cohesionDv *= maxCohesionDv / cohesionSpeed;
|
||||
v += cohesionDv;
|
||||
}
|
||||
|
||||
v += vec3(pc.d[5], pc.d[6], pc.d[7]) * dt;
|
||||
v *= max(0.0, 1.0 - pc.d[12] * dt);
|
||||
if (pc.d[11] > 0.0) v *= exp(-pc.d[11] * 300.0 * dt);
|
||||
float sp = length(v);
|
||||
float vmax = pc.d[13];
|
||||
if (sp > vmax && sp > 1e-9) v *= vmax / sp;
|
||||
p += v * dt;
|
||||
|
||||
float radius = pc.d[2];
|
||||
float d = sdfSample(p);
|
||||
vec3 nn = sdfGrad(p);
|
||||
float nl = length(nn);
|
||||
if (nl > 1e-6) nn /= nl;
|
||||
else nn = vec3(0.0, 1.0, 0.0);
|
||||
if (d < radius) {
|
||||
p = p - nn * (d - radius);
|
||||
float vn = dot(v, nn);
|
||||
if (vn < 0.0) v -= nn * vn;
|
||||
d = radius;
|
||||
}
|
||||
float adh = pc.d[11];
|
||||
if (adh > 0.0 && d < h) v += -nn * (adh * cohesionKernel(d, h) * dt);
|
||||
|
||||
pos.p[i] = vec4(p, 0.0);
|
||||
vel.v[i] = vec4(v, 0.0);
|
||||
grad.g[i] = vec4(v, 0.0);
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
|
||||
@@ -51,18 +51,22 @@ float triangleDistance(const glm::vec3& p, const glm::vec3& a, const glm::vec3&
|
||||
return glm::length(p - (a + ab * v + ac * w));
|
||||
}
|
||||
|
||||
/** @brief Even-odd raycast along +X; returns true when p is inside the mesh. */
|
||||
/** @brief Even-odd raycast along a skew direction; returns true when p is inside the mesh. */
|
||||
bool pointInsideMesh(const glm::vec3& p, const std::vector<glm::vec3>& pos, const std::vector<uint32_t>& idx,
|
||||
int triCount) {
|
||||
// Axis rays frequently pass through shared vertices/edges in symmetric meshes,
|
||||
// double-counting one crossing. A fixed irrational-looking direction preserves
|
||||
// deterministic baking while avoiding those systematic degeneracies.
|
||||
const glm::vec3 direction = glm::normalize(glm::vec3(1.f, .37139067f, .127831f));
|
||||
int hits = 0;
|
||||
for (int tri = 0; tri < triCount; ++tri) {
|
||||
const glm::vec3 a = pos[idx[uint32_t(tri) * 3 + 0]];
|
||||
const glm::vec3 b = pos[idx[uint32_t(tri) * 3 + 1]];
|
||||
const glm::vec3 c = pos[idx[uint32_t(tri) * 3 + 2]];
|
||||
// Moller-Trumbore ray from p along +X.
|
||||
// Moller-Trumbore ray from p along the fixed skew direction.
|
||||
const glm::vec3 e1 = b - a;
|
||||
const glm::vec3 e2 = c - a;
|
||||
const glm::vec3 h = glm::cross(glm::vec3(1.f, 0.f, 0.f), e2);
|
||||
const glm::vec3 h = glm::cross(direction, e2);
|
||||
const float det = glm::dot(e1, h);
|
||||
if (std::fabs(det) < 1e-12f) continue;
|
||||
const float invDet = 1.f / det;
|
||||
@@ -70,7 +74,7 @@ bool pointInsideMesh(const glm::vec3& p, const std::vector<glm::vec3>& pos, cons
|
||||
const float u = invDet * glm::dot(s, h);
|
||||
if (u < 0.f || u > 1.f) continue;
|
||||
const glm::vec3 q = glm::cross(s, e1);
|
||||
const float v = invDet * glm::dot(glm::vec3(1.f, 0.f, 0.f), q);
|
||||
const float v = invDet * glm::dot(direction, q);
|
||||
if (v < 0.f || u + v > 1.f) continue;
|
||||
const float t = invDet * glm::dot(e2, q);
|
||||
if (t > 1e-8f) ++hits;
|
||||
@@ -88,38 +92,40 @@ bool MeshSdf::inBounds(const glm::ivec3& c) const {
|
||||
return c.x >= 0 && c.y >= 0 && c.z >= 0 && c.x < dims.x && c.y < dims.y && c.z < dims.z;
|
||||
}
|
||||
|
||||
float MeshSdf::sample(const glm::vec3& p) const {
|
||||
if (voxelCount() <= 0) return FLT_MAX;
|
||||
const glm::vec3 f = (p - origin) / cellSize;
|
||||
const glm::ivec3 i0 = glm::clamp(glm::ivec3(glm::floor(f)), glm::ivec3(0), dims - glm::ivec3(1));
|
||||
const glm::ivec3 i1 = glm::min(i0 + glm::ivec3(1), dims - glm::ivec3(1));
|
||||
const glm::vec3 t = glm::fract(f);
|
||||
float MeshSdf::sample(const glm::vec3& p) const { return sampleWithGradient(p).distance; }
|
||||
|
||||
const float v000 = distances[index(i0.x, i0.y, i0.z)];
|
||||
const float v100 = distances[index(i1.x, i0.y, i0.z)];
|
||||
const float v010 = distances[index(i0.x, i1.y, i0.z)];
|
||||
const float v110 = distances[index(i1.x, i1.y, i0.z)];
|
||||
const float v001 = distances[index(i0.x, i0.y, i1.z)];
|
||||
const float v101 = distances[index(i1.x, i0.y, i1.z)];
|
||||
const float v011 = distances[index(i0.x, i1.y, i1.z)];
|
||||
const float v111 = distances[index(i1.x, i1.y, i1.z)];
|
||||
|
||||
const float c00 = v000 + (v100 - v000) * t.x;
|
||||
const float c10 = v010 + (v110 - v010) * t.x;
|
||||
const float c01 = v001 + (v101 - v001) * t.x;
|
||||
const float c11 = v011 + (v111 - v011) * t.x;
|
||||
const float c0 = c00 + (c10 - c00) * t.y;
|
||||
const float c1 = c01 + (c11 - c01) * t.y;
|
||||
return c0 + (c1 - c0) * t.z;
|
||||
MeshSdfSample MeshSdf::sampleWithGradient(const glm::vec3& p) const {
|
||||
if (dims.x < 2 || dims.y < 2 || dims.z < 2 || !(cellSize > 0.f) || distances.size() != size_t(voxelCount()))
|
||||
return {FLT_MAX, glm::vec3(0.f, 1.f, 0.f)};
|
||||
const glm::vec3 maximum = origin + glm::vec3(dims - glm::ivec3(1)) * cellSize;
|
||||
const glm::vec3 clampedPoint = glm::clamp(p, origin, maximum);
|
||||
const glm::vec3 outside = p - clampedPoint;
|
||||
const glm::vec3 f = (clampedPoint - origin) / cellSize;
|
||||
const glm::ivec3 i0 = glm::min(glm::ivec3(glm::floor(f)), dims - glm::ivec3(2));
|
||||
const glm::ivec3 i1 = i0 + glm::ivec3(1);
|
||||
const glm::vec3 t = glm::clamp(f - glm::vec3(i0), glm::vec3(0.f), glm::vec3(1.f));
|
||||
const float v000 = distances[size_t(index(i0.x, i0.y, i0.z))];
|
||||
const float v100 = distances[size_t(index(i1.x, i0.y, i0.z))];
|
||||
const float v010 = distances[size_t(index(i0.x, i1.y, i0.z))];
|
||||
const float v110 = distances[size_t(index(i1.x, i1.y, i0.z))];
|
||||
const float v001 = distances[size_t(index(i0.x, i0.y, i1.z))];
|
||||
const float v101 = distances[size_t(index(i1.x, i0.y, i1.z))];
|
||||
const float v011 = distances[size_t(index(i0.x, i1.y, i1.z))];
|
||||
const float v111 = distances[size_t(index(i1.x, i1.y, i1.z))];
|
||||
const auto mix = [](float a, float b, float u) { return a + (b - a) * u; };
|
||||
const float x00 = mix(v000, v100, t.x), x10 = mix(v010, v110, t.x);
|
||||
const float x01 = mix(v001, v101, t.x), x11 = mix(v011, v111, t.x);
|
||||
const float y0 = mix(x00, x10, t.y), y1 = mix(x01, x11, t.y);
|
||||
const float distance = mix(y0, y1, t.z);
|
||||
const float dx = mix(mix(v100 - v000, v110 - v010, t.y), mix(v101 - v001, v111 - v011, t.y), t.z) / cellSize;
|
||||
const float dy = mix(mix(v010 - v000, v110 - v100, t.x), mix(v011 - v001, v111 - v101, t.x), t.z) / cellSize;
|
||||
const float dz = mix(mix(v001 - v000, v101 - v100, t.x), mix(v011 - v010, v111 - v110, t.x), t.y) / cellSize;
|
||||
const float outsideDistance = glm::length(outside);
|
||||
return outsideDistance > 1e-7f ? MeshSdfSample{distance + outsideDistance, outside / outsideDistance}
|
||||
: MeshSdfSample{distance, {dx, dy, dz}};
|
||||
}
|
||||
|
||||
glm::vec3 MeshSdf::gradient(const glm::vec3& p) const {
|
||||
const float e = cellSize;
|
||||
return glm::vec3(sample(p + glm::vec3(e, 0.f, 0.f)) - sample(p - glm::vec3(e, 0.f, 0.f)),
|
||||
sample(p + glm::vec3(0.f, e, 0.f)) - sample(p - glm::vec3(0.f, e, 0.f)),
|
||||
sample(p + glm::vec3(0.f, 0.f, e)) - sample(p - glm::vec3(0.f, 0.f, e))) /
|
||||
(2.f * e);
|
||||
}
|
||||
glm::vec3 MeshSdf::gradient(const glm::vec3& p) const { return sampleWithGradient(p).gradient; }
|
||||
|
||||
MeshSdf MeshSdf::makeSphere(const glm::vec3& center, float radius, const glm::ivec3& dims) {
|
||||
MeshSdf sdf;
|
||||
@@ -207,9 +213,20 @@ MeshSdf MeshSdf::makeFromTriangles(const std::vector<glm::vec3>& positions, cons
|
||||
const glm::vec3 p = sdf.origin + glm::vec3(float(x), float(y), float(z)) * cell;
|
||||
const size_t i = size_t(sdf.index(x, y, z));
|
||||
if (sdf.distances[i] >= FLT_MAX) {
|
||||
// No triangle in range: sign still matters far outside.
|
||||
sdf.distances[i] = pointInsideMesh(p, positions, indices, triCount) ? -cell : cell;
|
||||
} else if (pointInsideMesh(p, positions, indices, triCount)) {
|
||||
// The narrow triangle sweep leaves deep interior and far exterior
|
||||
// cells untouched. Resolve those cells exactly once during baking;
|
||||
// substituting one cell width destroys the SDF magnitude and makes
|
||||
// collision projection depend on grid resolution.
|
||||
float distance = FLT_MAX;
|
||||
for (int tri = 0; tri < triCount; ++tri) {
|
||||
const glm::vec3 a = positions[indices[uint32_t(tri) * 3 + 0]];
|
||||
const glm::vec3 b = positions[indices[uint32_t(tri) * 3 + 1]];
|
||||
const glm::vec3 c = positions[indices[uint32_t(tri) * 3 + 2]];
|
||||
distance = std::min(distance, triangleDistance(p, a, b, c));
|
||||
}
|
||||
sdf.distances[i] = distance;
|
||||
}
|
||||
if (pointInsideMesh(p, positions, indices, triCount)) {
|
||||
sdf.distances[i] = -sdf.distances[i];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
|
||||
namespace eve::fluids {
|
||||
|
||||
/** @brief One trilinear SDF sample and its local-space analytic gradient. */
|
||||
struct MeshSdfSample {
|
||||
float distance = 0.f;
|
||||
glm::vec3 gradient{0.f, 1.f, 0.f};
|
||||
};
|
||||
|
||||
/** @brief Uniform signed-distance voxel field over a box. */
|
||||
class MeshSdf {
|
||||
public:
|
||||
@@ -40,12 +46,20 @@ public:
|
||||
/**
|
||||
* @brief Trilinearly interpolated signed distance at p.
|
||||
* @param p world position.
|
||||
* @return distance, clamped at the field boundary.
|
||||
* @return distance, extended by Euclidean distance beyond the field boundary.
|
||||
*/
|
||||
float sample(const glm::vec3& p) const;
|
||||
|
||||
/**
|
||||
* @brief Central-difference gradient (outward normal) at p.
|
||||
* @brief Samples distance and analytic trilinear gradient from the same eight voxels.
|
||||
* @param p world
|
||||
* position.
|
||||
* @return distance and outward gradient at p.
|
||||
*/
|
||||
MeshSdfSample sampleWithGradient(const glm::vec3& p) const;
|
||||
|
||||
/**
|
||||
* @brief Analytic trilinear gradient (outward normal) at p.
|
||||
* @param p world position.
|
||||
* @return gradient vector (not normalized).
|
||||
*/
|
||||
@@ -73,10 +87,11 @@ public:
|
||||
* @brief Voxelize a closed triangle mesh.
|
||||
*
|
||||
* Unsigned distance is swept triangle-by-triangle over each triangle's
|
||||
* expanded AABB; the sign comes from an even-odd raycast along +X.
|
||||
* expanded AABB, with exact evaluation for untouched cells; the sign comes
|
||||
* from a deterministic
|
||||
* skew-direction even-odd raycast.
|
||||
* @param positions triangle vertices, 3 floats each.
|
||||
* @param indices triangle indices, 3 ints per triangle.
|
||||
* @param triangleCount number of triangles.
|
||||
* @param dims voxel resolution.
|
||||
* @return signed field over the mesh's bounding box (padded one voxel).
|
||||
*/
|
||||
|
||||
@@ -143,7 +143,7 @@ void FluidSimulation::computeLambdas() {
|
||||
const float rho0 = std::max(params_.restDensity, 1e-6f);
|
||||
for (int i = 0; i < count_; ++i) {
|
||||
const float rho = densities_[size_t(i)];
|
||||
const float C = rho / rho0 - 1.f;
|
||||
const float C = std::max(0.f, rho / rho0 - 1.f);
|
||||
const float gradSq = glm::dot(gradSums_[size_t(i)], gradSums_[size_t(i)]);
|
||||
lambdas_[size_t(i)] = -C / (gradSq + 1e-6f);
|
||||
}
|
||||
@@ -180,12 +180,12 @@ void FluidSimulation::applyPositionCorrections() {
|
||||
// Clamp the PBF position correction (standard PBF stabilization):
|
||||
// overlapping particles would otherwise amplify the spiky gradient
|
||||
// unboundedly and fling the drop apart.
|
||||
const float maxDelta = 0.05f * h;
|
||||
const float maxDelta = 0.0005f * h;
|
||||
const glm::vec3 correction = delta / rho0;
|
||||
const float cl = glm::length(correction);
|
||||
if (cl > maxDelta && cl > 1e-9f) p = pi + correction * (maxDelta / cl);
|
||||
const float d = sdf_.sample(p);
|
||||
if (d < radius) {
|
||||
{
|
||||
glm::vec3 n = sdf_.gradient(p);
|
||||
const float nl = glm::length(n);
|
||||
if (nl > 1e-6f)
|
||||
@@ -211,6 +211,9 @@ void FluidSimulation::integrate(float dt) {
|
||||
glm::vec3 viscAcc(0.f);
|
||||
glm::vec3 cohesionAcc(0.f);
|
||||
float shearAcc = 0.f;
|
||||
float neighborWeight = 0.f;
|
||||
float cohesionWeight = 0.f;
|
||||
const float particleVolume = std::pow(2.f * radius, 3.f);
|
||||
const glm::ivec3 c = grid_.cellOf(p);
|
||||
for (int z = -1; z <= 1; ++z) {
|
||||
for (int y = -1; y <= 1; ++y) {
|
||||
@@ -225,13 +228,16 @@ void FluidSimulation::integrate(float dt) {
|
||||
const glm::vec3 dx = p - particles_[size_t(j)].pos;
|
||||
const float r2 = glm::dot(dx, dx);
|
||||
if (r2 < h2) {
|
||||
if (params_.viscosity > 0.f)
|
||||
viscAcc += (particles_[size_t(j)].vel - v) * fluidPoly6(r2, h);
|
||||
const float weight = fluidPoly6(r2, h) * particleVolume;
|
||||
if (params_.viscosity > 0.f || params_.yieldStress > 0.f) {
|
||||
viscAcc += (particles_[size_t(j)].vel - v) * weight;
|
||||
neighborWeight += weight;
|
||||
}
|
||||
if (params_.yieldStress > 0.f)
|
||||
shearAcc += glm::length(particles_[size_t(j)].vel - v) * fluidPoly6(r2, h);
|
||||
shearAcc += glm::length(particles_[size_t(j)].vel - v) * weight;
|
||||
if (params_.cohesion > 0.f) {
|
||||
const float r = std::sqrt(r2);
|
||||
cohesionAcc += -params_.cohesion * fluidCohesionKernel(r, h) * (dx / r);
|
||||
cohesionAcc -= dx * weight;
|
||||
cohesionWeight += weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,12 +248,20 @@ void FluidSimulation::integrate(float dt) {
|
||||
// so low-shear mud "freezes" and piles up instead of spreading.
|
||||
float effectiveViscosity = params_.viscosity;
|
||||
if (params_.yieldStress > 0.f && shearAcc > 1e-6f) effectiveViscosity += params_.yieldStress / shearAcc;
|
||||
v += viscAcc * (effectiveViscosity * dt);
|
||||
v += cohesionAcc * dt;
|
||||
if (neighborWeight > 1e-6f) v += (viscAcc / neighborWeight) * std::clamp(effectiveViscosity * dt, 0.f, 1.f);
|
||||
glm::vec3 cohesionNormal = sdf_.gradient(p);
|
||||
if (glm::length(cohesionNormal) > 1e-6f)
|
||||
cohesionNormal = glm::normalize(cohesionNormal);
|
||||
else
|
||||
cohesionNormal = glm::vec3(0.f, 1.f, 0.f);
|
||||
cohesionAcc -= cohesionNormal * glm::dot(cohesionAcc, cohesionNormal);
|
||||
if (cohesionWeight > 1e-6f)
|
||||
v += fluidClampSpeed((cohesionAcc / cohesionWeight) * (params_.cohesion * 100.f * dt), radius);
|
||||
}
|
||||
|
||||
v += params_.gravity * dt;
|
||||
v *= std::max(0.f, 1.f - params_.damping * dt);
|
||||
if (params_.adhesion > 0.f) v *= std::exp(-params_.adhesion * 300.f * dt);
|
||||
v = fluidClampSpeed(v, params_.maxVelocity);
|
||||
p += v * dt;
|
||||
|
||||
@@ -260,14 +274,14 @@ void FluidSimulation::integrate(float dt) {
|
||||
n /= nl;
|
||||
else
|
||||
n = glm::vec3(0.f, 1.f, 0.f);
|
||||
if (d < radius) {
|
||||
{
|
||||
p = p - n * (d - radius);
|
||||
const float vn = glm::dot(v, n);
|
||||
if (vn < 0.f) v -= n * vn;
|
||||
v -= n * vn;
|
||||
d = radius;
|
||||
}
|
||||
// Adhesion: pull the film toward the solid while within range.
|
||||
if (params_.adhesion > 0.f && d < h) v += -n * (params_.adhesion * fluidCohesionKernel(d, h) * dt);
|
||||
if (params_.adhesion > 0.f && d < h) v += -n * (params_.adhesion * std::max(0.f, 1.f - d / h) * dt * 30.f);
|
||||
|
||||
particles_[size_t(i)].pos = p;
|
||||
particles_[size_t(i)].vel = v;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* Push-constant layout (float[32]):
|
||||
* [0..15] viewProj (column-major, glm)
|
||||
* [16] particleCount
|
||||
* [17] pad
|
||||
* [17] orthographic (0 perspective, 1 orthographic)
|
||||
* [18] pad
|
||||
* [19] nearZ
|
||||
* [20] farZ
|
||||
@@ -22,11 +22,13 @@
|
||||
* [26] mode (0 water, 1 mud)
|
||||
* [27] thicknessScale
|
||||
* [28] depthFalloff (bilateral depth edge falloff)
|
||||
* [29..31] pad
|
||||
* [29] blurRadiusWorld (-1 keeps the established fixed radius)
|
||||
* [30..31] pad
|
||||
*
|
||||
* Buffer bindings:
|
||||
* 0 particles(vec4 xyz+radius) 1 depthA(uint) 2 depthB(uint)
|
||||
* 3 thickness(uint) 4 normal(vec4) 5 color(vec4)
|
||||
* 3 thickness(uint) 4 normal(vec4) 5 color(packed RGBA8 uint)
|
||||
* 6 particle colors(vec4) 7 multicolor accum(5 uint/pixel)
|
||||
*/
|
||||
|
||||
namespace eve::fluids {
|
||||
@@ -39,7 +41,7 @@ layout(set = 0, binding = 1) buffer DepthA { uint d[]; } depthA;
|
||||
layout(set = 0, binding = 2) buffer DepthB { uint d[]; } depthB;
|
||||
layout(set = 0, binding = 3) buffer Thick { uint t[]; } thick;
|
||||
layout(set = 0, binding = 4) buffer Normal { vec4 n[]; } normal;
|
||||
layout(set = 0, binding = 5) buffer Color { vec4 c[]; } color;
|
||||
layout(set = 0, binding = 5) buffer Color { uint c[]; } color;
|
||||
layout(push_constant) uniform PC { float d[32]; } pc;
|
||||
void main() {
|
||||
uint i = gl_GlobalInvocationID.x;
|
||||
@@ -49,7 +51,7 @@ void main() {
|
||||
depthB.d[i] = 0xFFFFFFFFu;
|
||||
thick.t[i] = 0u;
|
||||
normal.n[i] = vec4(0.0);
|
||||
color.c[i] = vec4(0.0);
|
||||
color.c[i] = 0u;
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
@@ -74,20 +76,20 @@ void main() {
|
||||
uint n = uint(pc.d[16]);
|
||||
if (i >= n) return;
|
||||
vec4 clip = loadVP() * vec4(parts.p[i].xyz, 1.0);
|
||||
if (clip.w <= 1e-4) return;
|
||||
bool orthographic = pc.d[17] > 0.5;
|
||||
if (!orthographic && clip.w <= 1e-4) return;
|
||||
vec3 ndc = clip.xyz / clip.w;
|
||||
if (any(lessThan(ndc, vec3(-1.0))) || any(greaterThan(ndc, vec3(1.0)))) return;
|
||||
float W = pc.d[23];
|
||||
float H = pc.d[24];
|
||||
float sx = (ndc.x * 0.5 + 0.5) * W;
|
||||
float sy = (0.5 - ndc.y * 0.5) * H;
|
||||
float depth = clip.w; // == -viewZ for a perspective projection
|
||||
float nearZ = max(pc.d[19], 1e-4);
|
||||
float farZ = max(pc.d[20], nearZ + 1e-3);
|
||||
float t = clamp((depth - nearZ) / (farZ - nearZ), 0.0, 1.0);
|
||||
uint key = uint(t * 16777215.0);
|
||||
float tanHalf = max(pc.d[21], 1e-4);
|
||||
float radiusPx = (pc.d[25] * (H * 0.5) / tanHalf) / max(depth, 1e-4);
|
||||
float depth = orthographic ? nearZ + (ndc.z * 0.5 + 0.5) * (farZ - nearZ) : clip.w;
|
||||
float radiusPx = (pc.d[25] * (H * 0.5) / tanHalf) /
|
||||
(orthographic ? 1.0 : max(depth, 1e-4));
|
||||
if (radiusPx < 0.5) return;
|
||||
int x0 = int(max(floor(sx - radiusPx), 0.0));
|
||||
int x1 = int(min(ceil(sx + radiusPx), W - 1.0));
|
||||
@@ -102,15 +104,126 @@ void main() {
|
||||
float q = (ddx * ddx + ddy * ddy) / r2;
|
||||
if (q >= 1.0) continue;
|
||||
uint idx = uint(yy) * uint(W) + uint(xx);
|
||||
// Spherical cap in the projected particle disc, not a flat billboard.
|
||||
float cap = pc.d[25] * sqrt(1.0 - q);
|
||||
float t = clamp((depth - cap - nearZ) / (farZ - nearZ), 0.0, 1.0);
|
||||
uint key = uint(t * 16777215.0);
|
||||
atomicMin(depthA.d[idx], key);
|
||||
float w = 1.0 - smoothstep(0.4, 1.0, sqrt(q));
|
||||
uint contribution = uint(clamp(w * 2.0 * pc.d[25] * scale * 256.0, 0.0, 16777215.0));
|
||||
uint contribution = uint(clamp(2.0 * cap * scale * 256.0, 0.0, 16777215.0));
|
||||
atomicAdd(thick.t[idx], contribution);
|
||||
}
|
||||
}
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
/**
|
||||
* @brief Clear lazily-created fixed-point multicolor accumulation buffers.
|
||||
* @lifetime The returned pointer remains valid for the process lifetime.
|
||||
* @return Process-lifetime static read-only GLSL source; the caller must not free it.
|
||||
*/
|
||||
inline const char* kSsfColorClear = R"GLSL(
|
||||
#version 450
|
||||
layout(local_size_x = 64) in;
|
||||
layout(set = 0, binding = 7) buffer Accum { uint v[]; } accum;
|
||||
layout(push_constant) uniform PC { float d[32]; } pc;
|
||||
void main() {
|
||||
uint i = gl_GlobalInvocationID.x;
|
||||
if (i >= uint(pc.d[23]) * uint(pc.d[24])) return;
|
||||
uint at = i * 5u;
|
||||
for (uint channel = 0u; channel < 5u; ++channel) accum.v[at + channel] = 0u;
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
/**
|
||||
* @brief Accumulate isotropic particle colors using the CPU tint pass' disc weights.
|
||||
* @lifetime The returned pointer remains valid for the process lifetime.
|
||||
* @return Process-lifetime static read-only GLSL source; the caller must not free it.
|
||||
*/
|
||||
inline const char* kSsfColorSplat = R"GLSL(
|
||||
#version 450
|
||||
layout(local_size_x = 64) in;
|
||||
layout(set = 0, binding = 0) buffer Particles { vec4 p[]; } parts;
|
||||
layout(set = 0, binding = 6) buffer ParticleColors { vec4 c[]; } particleColors;
|
||||
layout(set = 0, binding = 7) buffer Accum { uint v[]; } accum;
|
||||
layout(push_constant) uniform PC { float d[32]; } pc;
|
||||
mat4 loadVP() {
|
||||
return mat4(pc.d[0], pc.d[1], pc.d[2], pc.d[3], pc.d[4], pc.d[5], pc.d[6], pc.d[7],
|
||||
pc.d[8], pc.d[9], pc.d[10], pc.d[11], pc.d[12], pc.d[13], pc.d[14], pc.d[15]);
|
||||
}
|
||||
void main() {
|
||||
uint i = gl_GlobalInvocationID.x;
|
||||
if (i >= uint(pc.d[16])) return;
|
||||
vec4 clip = loadVP() * vec4(parts.p[i].xyz, 1.0);
|
||||
bool orthographic = pc.d[17] > 0.5;
|
||||
if (!orthographic && clip.w <= 1e-4) return;
|
||||
vec3 ndc = clip.xyz / clip.w;
|
||||
if (any(lessThan(ndc, vec3(-1.0))) || any(greaterThan(ndc, vec3(1.0)))) return;
|
||||
float W = pc.d[23], H = pc.d[24];
|
||||
float sx = (ndc.x * 0.5 + 0.5) * W;
|
||||
float sy = (0.5 - ndc.y * 0.5) * H;
|
||||
float depth = orthographic ? pc.d[19] + (ndc.z * 0.5 + 0.5) * (pc.d[20] - pc.d[19]) : clip.w;
|
||||
float radiusPx = (pc.d[25] * (H * 0.5) / max(pc.d[21], 1e-4)) /
|
||||
(orthographic ? 1.0 : max(depth, 1e-4));
|
||||
if (radiusPx < 0.5) return;
|
||||
int x0 = int(max(floor(sx - radiusPx), 0.0)), x1 = int(min(ceil(sx + radiusPx), W - 1.0));
|
||||
int y0 = int(max(floor(sy - radiusPx), 0.0)), y1 = int(min(ceil(sy + radiusPx), H - 1.0));
|
||||
vec4 tint = clamp(particleColors.c[i], 0.0, 1.0);
|
||||
float radiusSquared = radiusPx * radiusPx;
|
||||
for (int y = y0; y <= y1; ++y) for (int x = x0; x <= x1; ++x) {
|
||||
vec2 delta = vec2(float(x) + 0.5 - sx, float(y) + 0.5 - sy);
|
||||
float q = 1.0 - dot(delta, delta) / radiusSquared;
|
||||
if (q <= 0.0) continue;
|
||||
uint scaled = max(1u, uint(q * 4095.0));
|
||||
uint at = (uint(y) * uint(W) + uint(x)) * 5u;
|
||||
atomicAdd(accum.v[at + 0u], scaled);
|
||||
atomicAdd(accum.v[at + 1u], uint(float(scaled) * tint.r));
|
||||
atomicAdd(accum.v[at + 2u], uint(float(scaled) * tint.g));
|
||||
atomicAdd(accum.v[at + 3u], uint(float(scaled) * tint.b));
|
||||
atomicAdd(accum.v[at + 4u], uint(float(scaled) * tint.a));
|
||||
}
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
// Splat preprojected oriented ellipsoids without expanding push constants.
|
||||
inline constexpr auto kSsfAnisotropicSplat = R"GLSL(
|
||||
#version 450
|
||||
layout(local_size_x = 64) in;
|
||||
layout(set = 0, binding = 0) buffer Ellipsoids { vec4 e[]; } ellipsoids;
|
||||
layout(set = 0, binding = 1) buffer DepthA { uint d[]; } depthA;
|
||||
layout(set = 0, binding = 3) buffer Thick { uint t[]; } thick;
|
||||
layout(push_constant) uniform PC { float d[32]; } pc;
|
||||
|
||||
void main() {
|
||||
uint i = gl_GlobalInvocationID.x;
|
||||
if (i >= uint(pc.d[16])) return;
|
||||
vec4 projected = ellipsoids.e[i * 3u + 0u];
|
||||
vec4 inverseAndRadius = ellipsoids.e[i * 3u + 1u];
|
||||
vec4 depthSlope = ellipsoids.e[i * 3u + 2u];
|
||||
if (projected.w <= 0.0) return;
|
||||
float W = pc.d[23], H = pc.d[24];
|
||||
int x0 = int(max(floor(projected.x - projected.w), 0.0));
|
||||
int x1 = int(min(ceil(projected.x + projected.w), W - 1.0));
|
||||
int y0 = int(max(floor(projected.y - projected.w), 0.0));
|
||||
int y1 = int(min(ceil(projected.y + projected.w), H - 1.0));
|
||||
float nearZ = max(pc.d[19], 1e-4);
|
||||
float farZ = max(pc.d[20], nearZ + 1e-3);
|
||||
for (int yy = y0; yy <= y1; ++yy) for (int xx = x0; xx <= x1; ++xx) {
|
||||
vec2 delta = vec2(float(xx) + 0.5 - projected.x, float(yy) + 0.5 - projected.y);
|
||||
float q = inverseAndRadius.x * delta.x * delta.x +
|
||||
2.0 * inverseAndRadius.y * delta.x * delta.y +
|
||||
inverseAndRadius.z * delta.y * delta.y;
|
||||
if (q >= 1.0) continue;
|
||||
float cap = inverseAndRadius.w * sqrt(max(0.0, 1.0 - q));
|
||||
float centerDepth = projected.z + dot(depthSlope.xy, delta);
|
||||
float t = clamp((centerDepth - cap - nearZ) / (farZ - nearZ), 0.0, 1.0);
|
||||
uint idx = uint(yy) * uint(W) + uint(xx);
|
||||
atomicMin(depthA.d[idx], uint(t * 16777215.0));
|
||||
uint contribution = uint(clamp(2.0 * cap * pc.d[27] * 256.0, 0.0, 16777215.0));
|
||||
atomicAdd(thick.t[idx], contribution);
|
||||
}
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
/** @brief One bilateral smoothing pass (read depthA, write depthB). */
|
||||
inline const char* kSsfSmooth = R"GLSL(
|
||||
#version 450
|
||||
@@ -139,10 +252,14 @@ void main() {
|
||||
}
|
||||
float center = decode(centerKey);
|
||||
float falloff = max(pc.d[28], 1e-4); // depth edge falloff
|
||||
float projectedBlur = pc.d[29] < 0.0 ? 2.0 :
|
||||
pc.d[29] * float(H) / (2.0 * max(pc.d[21], 1e-4) * (pc.d[17] > 0.5 ? 1.0 : center));
|
||||
int kernelRadius = clamp(int(ceil(projectedBlur)), 0, 4);
|
||||
float sigma = max(0.5, projectedBlur * 0.5);
|
||||
float sum = center;
|
||||
float wsum = 1.0;
|
||||
for (int oy = -2; oy <= 2; ++oy) {
|
||||
for (int ox = -2; ox <= 2; ++ox) {
|
||||
for (int oy = -kernelRadius; oy <= kernelRadius; ++oy) {
|
||||
for (int ox = -kernelRadius; ox <= kernelRadius; ++ox) {
|
||||
if (ox == 0 && oy == 0) continue;
|
||||
int xx = int(cx) + ox;
|
||||
int yy = int(cy) + oy;
|
||||
@@ -151,7 +268,7 @@ void main() {
|
||||
uint nk = depthA.d[idx];
|
||||
if (nk == 0xFFFFFFFFu) continue;
|
||||
float nd = decode(nk);
|
||||
float spatial = exp(-(float(ox * ox + oy * oy)) * 0.5);
|
||||
float spatial = exp(-float(ox * ox + oy * oy) / (2.0 * sigma * sigma));
|
||||
float wDepth = exp(-abs(nd - center) / falloff);
|
||||
float w = spatial * wDepth;
|
||||
sum += nd * w;
|
||||
@@ -180,19 +297,30 @@ float decode(uint key) {
|
||||
return nearZ + (float(key) / 16777215.0) * (farZ - nearZ);
|
||||
}
|
||||
|
||||
float sampleDepth(ivec2 off, uint W, uint H, uint cx, uint cy, float fallback) {
|
||||
float sampleDepth(ivec2 off, uint W, uint H, uint cx, uint cy) {
|
||||
int xx = int(cx) + off.x;
|
||||
int yy = int(cy) + off.y;
|
||||
if (xx < 0 || yy < 0 || xx >= int(W) || yy >= int(H)) return fallback;
|
||||
if (xx < 0 || yy < 0 || xx >= int(W) || yy >= int(H)) return 1e30;
|
||||
uint k = depthA.d[uint(yy) * W + uint(xx)];
|
||||
return k == 0xFFFFFFFFu ? fallback : decode(k);
|
||||
return k == 0xFFFFFFFFu ? 1e30 : decode(k);
|
||||
}
|
||||
|
||||
vec3 surfaceDerivative(vec3 forward, vec3 backward, bool forwardValid, bool backwardValid) {
|
||||
if (!forwardValid && backwardValid) return backward;
|
||||
if (!backwardValid && forwardValid) return forward;
|
||||
// Preserve symmetry when differences are indistinguishable at depth-key precision.
|
||||
float tolerance = 2.0 * (pc.d[20] - pc.d[19]) / 16777215.0;
|
||||
if ((!forwardValid && !backwardValid) || abs(abs(forward.z) - abs(backward.z)) <= tolerance)
|
||||
return (forward + backward) * 0.5;
|
||||
return abs(forward.z) < abs(backward.z) ? forward : backward;
|
||||
}
|
||||
|
||||
vec3 viewPos(vec2 uv, float depth) {
|
||||
float tanHalf = max(pc.d[21], 1e-4);
|
||||
float aspect = max(pc.d[22], 1e-4);
|
||||
float x = (uv.x * 2.0 - 1.0) * aspect * tanHalf * depth;
|
||||
float y = (uv.y * 2.0 - 1.0) * tanHalf * depth;
|
||||
float scale = pc.d[17] > 0.5 ? 1.0 : depth;
|
||||
float x = (uv.x * 2.0 - 1.0) * aspect * tanHalf * scale;
|
||||
float y = (1.0 - uv.y * 2.0) * tanHalf * scale;
|
||||
return vec3(x, y, -depth);
|
||||
}
|
||||
|
||||
@@ -212,12 +340,17 @@ void main() {
|
||||
vec2 uv = vec2((float(cx) + 0.5) / float(W), (float(cy) + 0.5) / float(H));
|
||||
float texelW = 1.0 / float(W);
|
||||
float texelH = 1.0 / float(H);
|
||||
vec3 pL = viewPos(uv - vec2(texelW, 0.0), cd);
|
||||
vec3 pR = viewPos(uv + vec2(texelW, 0.0), cd);
|
||||
vec3 pD = viewPos(uv - vec2(0.0, texelH), cd);
|
||||
vec3 pU = viewPos(uv + vec2(0.0, texelH), cd);
|
||||
vec3 dpx = (pR - pL) * 0.5;
|
||||
vec3 dpy = (pU - pD) * 0.5;
|
||||
float dl = sampleDepth(ivec2(-1, 0), W, H, cx, cy);
|
||||
float dr = sampleDepth(ivec2(1, 0), W, H, cx, cy);
|
||||
float dt = sampleDepth(ivec2(0, -1), W, H, cx, cy);
|
||||
float db = sampleDepth(ivec2(0, 1), W, H, cx, cy);
|
||||
vec3 center = viewPos(uv, cd);
|
||||
vec3 pL = viewPos(uv - vec2(texelW, 0.0), dl < 1e29 ? dl : cd);
|
||||
vec3 pR = viewPos(uv + vec2(texelW, 0.0), dr < 1e29 ? dr : cd);
|
||||
vec3 pT = viewPos(uv - vec2(0.0, texelH), dt < 1e29 ? dt : cd);
|
||||
vec3 pB = viewPos(uv + vec2(0.0, texelH), db < 1e29 ? db : cd);
|
||||
vec3 dpx = surfaceDerivative(pR - center, center - pL, dr < 1e29, dl < 1e29);
|
||||
vec3 dpy = surfaceDerivative(pB - center, center - pT, db < 1e29, dt < 1e29);
|
||||
vec3 n = normalize(cross(dpx, dpy));
|
||||
if (n.z < 0.0) n = -n;
|
||||
normal.n[i] = vec4(n, 0.0);
|
||||
@@ -231,7 +364,8 @@ layout(local_size_x = 64) in;
|
||||
layout(set = 0, binding = 1) buffer DepthA { uint d[]; } depthA;
|
||||
layout(set = 0, binding = 3) buffer Thick { uint t[]; } thick;
|
||||
layout(set = 0, binding = 4) buffer Normal { vec4 n[]; } normal;
|
||||
layout(set = 0, binding = 5) buffer Color { vec4 c[]; } color;
|
||||
layout(set = 0, binding = 5) buffer Color { uint c[]; } color;
|
||||
layout(set = 0, binding = 7) buffer Accum { uint v[]; } accum;
|
||||
layout(push_constant) uniform PC { float d[32]; } pc;
|
||||
|
||||
void main() {
|
||||
@@ -240,7 +374,7 @@ void main() {
|
||||
uint H = uint(pc.d[24]);
|
||||
if (i >= W * H) return;
|
||||
if (depthA.d[i] == 0xFFFFFFFFu) {
|
||||
color.c[i] = vec4(0.0);
|
||||
color.c[i] = 0u;
|
||||
return;
|
||||
}
|
||||
vec3 n = normal.n[i].xyz;
|
||||
@@ -249,9 +383,44 @@ void main() {
|
||||
vec3 V = vec3(0.0, 0.0, 1.0);
|
||||
float diff = max(dot(n, L), 0.0);
|
||||
int mode = int(pc.d[26]);
|
||||
bool particleTint = mode >= 20;
|
||||
if (particleTint) mode -= 20;
|
||||
bool uniformTint = !particleTint && mode >= 10;
|
||||
if (uniformTint) mode -= 10;
|
||||
vec3 outC;
|
||||
float alpha;
|
||||
if (mode == 1) {
|
||||
if (uniformTint || particleTint) {
|
||||
uint accumulatedAt = i * 5u;
|
||||
uint accumulatedWeight = particleTint ? accum.v[accumulatedAt] : 1u;
|
||||
if (accumulatedWeight == 0u) {
|
||||
color.c[i] = 0u;
|
||||
return;
|
||||
}
|
||||
vec4 tint = particleTint
|
||||
? vec4(accum.v[accumulatedAt + 1u], accum.v[accumulatedAt + 2u],
|
||||
accum.v[accumulatedAt + 3u], accum.v[accumulatedAt + 4u]) / float(accumulatedWeight)
|
||||
: vec4(pc.d[0], pc.d[1], pc.d[2], pc.d[3]);
|
||||
vec3 reflectionColor = vec3(pc.d[4], pc.d[5], pc.d[6]);
|
||||
bool lighting = pc.d[7] > 0.5;
|
||||
float smoothness = pc.d[8];
|
||||
float metalness = pc.d[9];
|
||||
float ambient = pc.d[10];
|
||||
float reflection = pc.d[11];
|
||||
float opacity = pc.d[12];
|
||||
float cutoff = pc.d[13];
|
||||
if (thickness * 10.0 < cutoff) {
|
||||
color.c[i] = 0u;
|
||||
return;
|
||||
}
|
||||
float light = lighting ?
|
||||
clamp(ambient + (1.0 - min(ambient, 1.0)) * diff, 0.0, 6.0) : 1.0;
|
||||
float exponent = 4.0 + 124.0 * smoothness;
|
||||
float spec = lighting ? pow(max(n.z, 0.0), exponent) * smoothness * 0.56 : 0.0;
|
||||
float fresnel = 0.04 + 0.96 * pow(1.0 - max(n.z, 0.0), 5.0);
|
||||
vec3 reflected = mix(reflectionColor, tint.rgb, metalness);
|
||||
outC = tint.rgb * light + reflected * fresnel * reflection + vec3(spec);
|
||||
alpha = clamp(thickness * opacity, 0.0, 1.0) * tint.a;
|
||||
} else if (mode == 1) {
|
||||
// Mud: diffuse brown, darker with thickness, rough specular.
|
||||
vec3 base = vec3(0.36, 0.23, 0.12) * (0.45 + 0.55 * diff);
|
||||
float attenuation = exp(-thickness * 1.8);
|
||||
@@ -260,7 +429,6 @@ void main() {
|
||||
outC = base * attenuation + vec3(spec);
|
||||
alpha = clamp(thickness * 0.6, 0.0, 1.0);
|
||||
} else {
|
||||
// Water: blue base, Fresnel rim, sharp specular.
|
||||
vec3 base = vec3(0.05, 0.32, 0.72) * (0.55 + 0.45 * diff);
|
||||
float fresnel = 0.04 + 0.96 * pow(1.0 - max(dot(n, V), 0.0), 5.0);
|
||||
vec3 Hv = normalize(L + V);
|
||||
@@ -268,7 +436,9 @@ void main() {
|
||||
outC = base + vec3(0.55, 0.72, 1.0) * fresnel * 0.75 + vec3(spec);
|
||||
alpha = clamp(thickness * 0.35, 0.0, 1.0);
|
||||
}
|
||||
color.c[i] = vec4(outC, alpha);
|
||||
// Match the CPU output's clamp and truncation (packUnorm4x8 rounds instead).
|
||||
uvec4 rgba = uvec4(clamp(vec4(outC, alpha), 0.0, 1.0) * 255.0);
|
||||
color.c[i] = rgba.x | (rgba.y << 8u) | (rgba.z << 16u) | (rgba.w << 24u);
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "fluids/FluidSurfaceRenderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace eve::fluids {
|
||||
|
||||
glm::vec4 FluidSurfaceRenderer::blendFluidColor(FluidBlendFactor factor, const glm::vec4& source,
|
||||
const glm::vec4& destination) {
|
||||
switch (factor) {
|
||||
case FluidBlendFactor::Zero: return glm::vec4(0.f);
|
||||
case FluidBlendFactor::One: return glm::vec4(1.f);
|
||||
case FluidBlendFactor::DestinationColor: return destination;
|
||||
case FluidBlendFactor::SourceColor: return source;
|
||||
case FluidBlendFactor::OneMinusDestinationColor: return glm::vec4(1.f) - destination;
|
||||
case FluidBlendFactor::SourceAlpha: return glm::vec4(source.a);
|
||||
case FluidBlendFactor::OneMinusSourceColor: return glm::vec4(1.f) - source;
|
||||
case FluidBlendFactor::DestinationAlpha: return glm::vec4(destination.a);
|
||||
case FluidBlendFactor::OneMinusDestinationAlpha: return glm::vec4(1.f - destination.a);
|
||||
case FluidBlendFactor::SourceAlphaSaturate: {
|
||||
const float saturated = std::min(source.a, 1.f - destination.a);
|
||||
return glm::vec4(saturated, saturated, saturated, 1.f);
|
||||
}
|
||||
case FluidBlendFactor::OneMinusSourceAlpha: return glm::vec4(1.f - source.a);
|
||||
}
|
||||
return glm::vec4(0.f);
|
||||
}
|
||||
|
||||
Result<void> FluidSurfaceRenderer::configureSurfaceBlend(int source, int destination) {
|
||||
if (source < int(FluidBlendFactor::Zero) || source > int(FluidBlendFactor::OneMinusSourceAlpha) ||
|
||||
destination < int(FluidBlendFactor::Zero) || destination > int(FluidBlendFactor::OneMinusSourceAlpha))
|
||||
return Result<void>::failure(Diagnostic::error(
|
||||
DiagnosticCode::InvalidArgument, "Fluid surface blend factors must be Unity BlendMode values in [0,10]",
|
||||
"fluids.surface.blend"));
|
||||
surfaceBlendSource_ = static_cast<FluidBlendFactor>(source);
|
||||
surfaceBlendDestination_ = static_cast<FluidBlendFactor>(destination);
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
Result<void> FluidSurfaceRenderer::compositeConfiguredSurfaceBlend(std::span<const uint8_t> sceneColor) {
|
||||
const size_t expected = size_t(params_.width) * size_t(params_.height) * 4u;
|
||||
if (!auxiliaryCurrent_ || residentColorCurrent_)
|
||||
return Result<void>::failure(Diagnostic::error(DiagnosticCode::PreconditionViolation,
|
||||
"Current fluid frame has no host-visible surface color",
|
||||
"fluids.surface.blend"));
|
||||
if (sceneColor.size() != expected)
|
||||
return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Surface blend requires a matching RGBA8 scene image",
|
||||
"fluids.surface.blend"));
|
||||
|
||||
for (size_t offset = 0; offset < expected; offset += 4u) {
|
||||
glm::vec4 source, destination;
|
||||
for (size_t channel = 0; channel < 4u; ++channel) {
|
||||
source[channel] = float(color_[offset + channel]) / 255.f;
|
||||
destination[channel] = float(sceneColor[offset + channel]) / 255.f;
|
||||
}
|
||||
const glm::vec4 blended =
|
||||
glm::clamp(source * blendFluidColor(surfaceBlendSource_, source, destination) +
|
||||
destination * blendFluidColor(surfaceBlendDestination_, source, destination),
|
||||
glm::vec4(0.f), glm::vec4(1.f));
|
||||
for (size_t channel = 0; channel < 4u; ++channel)
|
||||
color_[offset + channel] = uint8_t(blended[channel] * 255.f + .5f);
|
||||
}
|
||||
residentColorCurrent_ = false;
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
Result<void> FluidSurfaceRenderer::configureParticleBlend(int source, int destination, bool depthWrite) {
|
||||
if (source < int(FluidBlendFactor::Zero) || source > int(FluidBlendFactor::OneMinusSourceAlpha) ||
|
||||
destination < int(FluidBlendFactor::Zero) || destination > int(FluidBlendFactor::OneMinusSourceAlpha))
|
||||
return Result<void>::failure(Diagnostic::error(
|
||||
DiagnosticCode::InvalidArgument, "Fluid particle blend factors must be Unity BlendMode values in [0,10]",
|
||||
"fluids.surface.particleBlend"));
|
||||
particleBlendSource_ = static_cast<FluidBlendFactor>(source);
|
||||
particleBlendDestination_ = static_cast<FluidBlendFactor>(destination);
|
||||
particleDepthWrite_ = depthWrite;
|
||||
particleBlendConfigured_ = true;
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
} // namespace eve::fluids
|
||||
File diff suppressed because it is too large.
Load diff
@@ -6,13 +6,19 @@
|
||||
* Splats particles into a depth/thickness buffer, bilaterally smooths the
|
||||
* depth (curvature-flow style), reconstructs normals from depth gradients and
|
||||
* shades the result as either water (Fresnel + refraction look) or mud
|
||||
* (diffuse, thickness-attenuated). The CPU reference mirrors the GLSL kernels
|
||||
* in FluidSsfKernels.h 1:1; the GPU path runs on the gpgpu compute queue.
|
||||
* (diffuse, thickness-attenuated). Projected particle discs carry spherical-cap
|
||||
* depth and chord thickness. The CPU reference mirrors the GLSL algorithms in
|
||||
* FluidSsfKernels.h; GPU depth and thickness use fixed-point quantization.
|
||||
* This disc approximation is not an exact perspective ray/sphere intersection.
|
||||
* The GPU path runs on the gpgpu compute queue.
|
||||
*/
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include "common/Result.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -23,9 +29,31 @@ class GpuBuffer;
|
||||
class Sequence;
|
||||
} // namespace eve::gpgpu
|
||||
|
||||
namespace eve::graphics {
|
||||
class Graphics;
|
||||
class Texture;
|
||||
} // namespace eve::graphics
|
||||
|
||||
namespace eve::fluids {
|
||||
|
||||
class FluidSimulator;
|
||||
class VolumeFluid;
|
||||
class VolumeFluidDiffuse;
|
||||
|
||||
/** @brief Unity/Fluid3D blend factors used by fluid render settings. */
|
||||
enum class FluidBlendFactor : int {
|
||||
Zero = 0,
|
||||
One = 1,
|
||||
DestinationColor = 2,
|
||||
SourceColor = 3,
|
||||
OneMinusDestinationColor = 4,
|
||||
SourceAlpha = 5,
|
||||
OneMinusSourceColor = 6,
|
||||
DestinationAlpha = 7,
|
||||
OneMinusDestinationAlpha = 8,
|
||||
SourceAlphaSaturate = 9,
|
||||
OneMinusSourceAlpha = 10,
|
||||
};
|
||||
|
||||
/** @brief Camera + reconstruction tuning for the SSF pipeline. */
|
||||
struct FluidSurfaceParams {
|
||||
@@ -33,6 +61,8 @@ struct FluidSurfaceParams {
|
||||
glm::vec3 target{0.f};
|
||||
glm::vec3 up{0.f, 1.f, 0.f};
|
||||
float fovYDeg = 55.f;
|
||||
bool orthographic = false;
|
||||
float orthographicSize = 1.f;
|
||||
float aspect = 1.f;
|
||||
float nearZ = 0.05f;
|
||||
float farZ = 20.f;
|
||||
@@ -46,6 +76,27 @@ struct FluidSurfaceParams {
|
||||
int mode = 0;
|
||||
};
|
||||
|
||||
/** @brief Complete owning mirror of Fluid3DRendererSettings with package defaults. */
|
||||
struct FluidRendererSettings {
|
||||
int blendSource = 5, blendDestination = 10;
|
||||
int particleBlendSource = 2, particleBlendDestination = 0;
|
||||
bool particleZWrite = false;
|
||||
float thicknessCutoff = 1.2f;
|
||||
int thicknessDownsample = 2;
|
||||
bool generateSurface = true;
|
||||
float blurRadius = .02f;
|
||||
int surfaceDownsample = 1;
|
||||
bool lighting = true;
|
||||
float smoothness = .8f, metalness = 0.f, ambientMultiplier = 1.f;
|
||||
bool generateReflection = true;
|
||||
float reflection = .2f;
|
||||
bool generateRefraction = true;
|
||||
float transparency = 1.f, absorption = 5.f, refraction = .01f;
|
||||
int refractionDownsample = 1;
|
||||
bool generateFoam = true;
|
||||
int foamDownsample = 1;
|
||||
};
|
||||
|
||||
/** @brief Buffer-level screen-space fluid renderer. */
|
||||
class FluidSurfaceRenderer {
|
||||
public:
|
||||
@@ -64,8 +115,279 @@ public:
|
||||
|
||||
/** @brief Reconstruct a frame from a simulator's live particles. */
|
||||
void render(const FluidSimulator& sim);
|
||||
/** @brief Reconstructs a volume-fluid snapshot and shades its diffusing particle colors. */
|
||||
void renderVolume(const VolumeFluid& sim);
|
||||
/** @brief Reconstructs from fixed-step interpolated volume positions using the existing SSF pipeline.
|
||||
* @param sim Borrowed solver; no pointer is retained.
|
||||
* @param alpha Finite fraction in [0,1] between previous and current successful step endpoints.
|
||||
* @return Success, or invalid argument before replacing renderer outputs.
|
||||
* @details Position interpolation is fused into the existing CPU upload-data copy. It adds no
|
||||
* reconstruction pass, buffer, texture, descriptor or GPU submission. Render-thread affine.
|
||||
*/
|
||||
[[nodiscard]] Result<void> renderVolumeInterpolated(const VolumeFluid& sim, float alpha);
|
||||
/**
|
||||
* @brief Reconstructs only the RGBA output for display, skipping auxiliary GPU readback when color is uniform.
|
||||
* @param sim Borrowed simulator read during this call only.
|
||||
* @lifetime The simulator is borrowed for the duration of this call and is never retained.
|
||||
* @details Render-thread affine. depth(), normals(), and thickness() remain unchanged; do not call
|
||||
* compositeDiffuse() after this method. Multicolor frames transparently use the full renderVolume path.
|
||||
*/
|
||||
void renderVolumeColorOnly(const VolumeFluid& sim);
|
||||
/**
|
||||
* @brief Renders gas particles as a blurred, translucent thickness volume.
|
||||
* @param sim Borrowed simulator read during this call only.
|
||||
* @param absorption Beer-Lambert extinction coefficient in [0.01,30].
|
||||
* @return Success, or a structured error when inputs or the fixed frame budget are exceeded.
|
||||
* @lifetime The simulator is borrowed for this call and is never retained.
|
||||
* @details Render-thread affine. Selects Gas particles only and does not run liquid surface
|
||||
* reconstruction or submit GPU work. The frame is limited to 65536 particles and four million
|
||||
* quarter-resolution projected bounding-box pixels; a failure preserves the previous output. Reuses frame scratch.
|
||||
*/
|
||||
[[nodiscard]] Result<void> renderGasVolume(const VolumeFluid& sim, float absorption = 5.f);
|
||||
/**
|
||||
* @brief Renders every material phase as a blurred color/thickness volume without reconstructing a surface.
|
||||
* @param sim Borrowed simulator read during this call only.
|
||||
* @param absorption Beer-Lambert extinction coefficient in [0.01,30].
|
||||
* @return Success, or a structured error when inputs or the fixed frame budget are exceeded.
|
||||
* @lifetime The simulator and its particle view are borrowed for this call and are never retained.
|
||||
* @details Implements Fluid3D's generateSurface=false presentation contract. Render-thread affine; performs no
|
||||
* GPU work and disables surface lighting implicitly by producing a volume frame directly. The frame is limited
|
||||
* to 65536 particles and four million quarter-resolution projected bounding-box pixels. Scratch storage is reused,
|
||||
* and a budget failure preserves the previous output.
|
||||
*/
|
||||
[[nodiscard]] Result<void> renderVolumeWithoutSurface(const VolumeFluid& sim, float absorption = 5.f);
|
||||
/** @brief Renders through Fluid3D's configured generateSurface policy.
|
||||
* @param sim Borrowed simulator read only for this render-thread call.
|
||||
* @return Success, or a structured budget failure from the surface-disabled volume path.
|
||||
* @details Enabled uses the existing SSF reconstruction. Disabled uses the existing bounded
|
||||
* all-phase thickness volume, skipping surface depth smoothing, normals, lighting, reflection,
|
||||
* refraction and GPU work. No simulator or callback is retained.
|
||||
*/
|
||||
[[nodiscard]] Result<void> renderConfiguredVolume(const VolumeFluid& sim);
|
||||
/** @brief Prepares the preferred reconstruction backend before frame timing begins.
|
||||
* @details Render-thread affine. GPU resource and shader creation is attempted once;
|
||||
* an unavailable GPU leaves the renderer on its CPU implementation. No simulation or
|
||||
* output state is changed. Call usingGpu() afterwards to observe the selected path.
|
||||
*/
|
||||
[[nodiscard]] Result<void> prepare();
|
||||
/** @brief Present the current RGBA8 result into an existing graphics texture.
|
||||
* @param graphics Borrowed active graphics module.
|
||||
* @param texture Borrowed matching single-mip texture owned by graphics.
|
||||
* @return Success after presentation, or a structured argument/upload failure.
|
||||
* @details Uses an allocation-free device-local copy when the current frame was shaded
|
||||
* on the GPU without CPU post-processing. Other backends and modified frames use the
|
||||
* existing host color upload. No resource or pointer is retained.
|
||||
* @thread Render-thread affine and synchronous.
|
||||
*/
|
||||
[[nodiscard]] Result<void> copyToTexture(graphics::Graphics* graphics, graphics::Texture* texture);
|
||||
/** @brief Reconstruct and present a uniform-color volume without host color readback when supported.
|
||||
* @param sim Borrowed solver used only during this call.
|
||||
* @param graphics Borrowed active graphics module.
|
||||
* @param texture Borrowed matching destination texture.
|
||||
* @return Success after reconstruction and presentation, or a structured failure.
|
||||
* @details Vulkan keeps the shaded RGBA8 buffer device-local. Unsupported backends retain the
|
||||
* normal host-visible color contract and upload fallback. Do not insert CPU color compositing
|
||||
* between reconstruction and presentation; use renderVolume plus copyToTexture for those modes.
|
||||
*/
|
||||
[[nodiscard]] Result<void> renderVolumeColorToTexture(const VolumeFluid& sim, graphics::Graphics* graphics,
|
||||
graphics::Texture* texture);
|
||||
/** @brief Composites white secondary particles over the last reconstructed frame.
|
||||
* @param pool Borrowed secondary particles; no pointer is retained or state advanced.
|
||||
* @param radius World-space splat radius in (0,1].
|
||||
* @param opacity Peak alpha in [0,1].
|
||||
* @param fadeSeconds Remaining-life interval in (0,86400] used for fading.
|
||||
* @details Render-thread affine, no concurrent calls or callbacks. Call after
|
||||
* render/renderVolume with the same camera, before image upload. Modifies only
|
||||
* RGBA output, preserving liquid depth/normals/thickness. Tests liquid depth,
|
||||
* not external scene geometry. Repeated calls accumulate until the next render.
|
||||
* Preflights at most 4M covered bounding-box pixels; failure leaves color intact.
|
||||
* Uses CPU buffers already read back by SSF; no new GPU submission or readback.
|
||||
*/
|
||||
[[nodiscard]] Result<void> compositeDiffuse(const VolumeFluidDiffuse& pool, float radius, float opacity,
|
||||
float fadeSeconds);
|
||||
/** @brief Configures Fluid3D-compatible foam generation visibility and render-target downsampling.
|
||||
* @param enabled When false, compositeDiffuse returns success without touching the current frame.
|
||||
* @param downsample Integer foam target divisor in [1,4].
|
||||
* @return Success, or invalid argument without changing prior configuration.
|
||||
* @details Render-thread affine. Configuration allocates nothing. Factors above one reduce particle splat
|
||||
* visits on each axis, reuse one owned scalar coverage target, then composite through one linear output pass.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureFoam(bool enabled, int downsample = 1);
|
||||
|
||||
/** @brief Script-friendly wrapper: render(*sim) with a null check. */
|
||||
/** @brief Removes fluid and diffuse color hidden by borrowed linear scene depth.
|
||||
* @param sceneDepth One finite, nonnegative view-space depth per output pixel; use a large finite value for no
|
||||
* geometry.
|
||||
* @param depthBias Nonnegative view-space tolerance in [0,1] metres for coincident surfaces.
|
||||
* @return Success, or a structured error before any color mutation.
|
||||
* @lifetime sceneDepth is borrowed for this call and is never retained.
|
||||
* @details Render-thread affine. Call after full render/renderVolume/renderGasVolume and optional
|
||||
* compositeDiffuse. This performs one allocation-free linear pixel pass and changes only RGBA.
|
||||
* The color-only GPU fast path has no current host depth and is rejected explicitly.
|
||||
*/
|
||||
[[nodiscard]] Result<void> occludeWithSceneDepth(std::span<const float> sceneDepth, float depthBias = 0.001f);
|
||||
|
||||
/** @brief Refracts a matching opaque RGBA8 scene through the current liquid surface.
|
||||
* @param sceneColor Borrowed width*height*4 RGBA8 background; it is never retained.
|
||||
* @param distortion Maximum screen-space displacement in pixels, in [0,64].
|
||||
* @param absorption Beer-Lambert absorption coefficient in [0,30].
|
||||
* @return Success, or a structured error before any output mutation.
|
||||
* @details Render-thread affine. Call after a full render/renderVolume. Performs one
|
||||
* allocation-free bounded pixel pass, modifies only RGBA output, and submits no GPU work.
|
||||
* The color-only GPU fast path is rejected because it has no current host auxiliaries.
|
||||
*/
|
||||
[[nodiscard]] Result<void> compositeSceneRefraction(std::span<const uint8_t> sceneColor, float distortion = 8.f,
|
||||
float absorption = 3.f);
|
||||
/** @brief Configure Fluid3D-compatible scene refraction controls atomically.
|
||||
* @param transparency Blend between particle color and refracted scene in [0,1].
|
||||
* @param absorption Per-channel Beer-Lambert coefficient in [0,30].
|
||||
* @param coefficient Signed normalized-screen bend coefficient in [-0.1,0.1].
|
||||
* @param downsample Refraction scene target divisor in [1,4].
|
||||
* @return Success, or invalid argument without changing prior configuration.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureRefraction(float transparency, float absorption, float coefficient,
|
||||
int downsample);
|
||||
/** @brief Enables or disables Fluid3D's configured refraction stage without losing its controls.
|
||||
* @param enabled When false, compositeConfiguredSceneRefraction is a zero-work no-op.
|
||||
* @return Success. The current frame and owned scratch are unchanged.
|
||||
* @details Render-thread affine; no allocation, scene copy or GPU work occurs during configuration.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureRefractionEnabled(bool enabled);
|
||||
/** @brief Apply the configured Fluid3D-compatible refraction to a matching opaque RGBA8 scene.
|
||||
* @param sceneColor Borrowed width*height*4 background bytes.
|
||||
* @return Success after one bounded composition, or failure before color mutation.
|
||||
* @details Reuses an owned target no larger than the output, submits no GPU work and retains no input.
|
||||
*/
|
||||
[[nodiscard]] Result<void> compositeConfiguredSceneRefraction(std::span<const uint8_t> sceneColor);
|
||||
|
||||
/** @brief Atomically configures Fluid3D's final surface source and destination blend factors.
|
||||
* @param source Unity BlendMode integer in [0,10].
|
||||
* @param destination Unity BlendMode integer in [0,10].
|
||||
* @return Success, or invalid argument without changing the prior pair.
|
||||
* @details Configuration allocates nothing and creates no graphics pipeline. The package defaults are
|
||||
* SourceAlpha value 5 and OneMinusSourceAlpha value 10.
|
||||
* @ownership No external resource is accepted or retained.
|
||||
* @lifetime The selected value pair lives with this renderer.
|
||||
* @thread Render-thread affine; do not race configuration with rendering.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureSurfaceBlend(int source, int destination);
|
||||
/** @brief Composites the current surface over a matching RGBA8 scene using the configured Fluid3D factors.
|
||||
* @param sceneColor Borrowed width*height*4 destination bytes; never retained.
|
||||
* @return Success after one bounded linear host pass, or failure before output mutation.
|
||||
* @details Supports every Unity BlendMode factor without creating parameterized Vulkan pipelines.
|
||||
* Render-thread affine. The resident color-only GPU path is rejected because host RGBA is not current.
|
||||
* @ownership No ownership transfer occurs.
|
||||
* @lifetime The input span is borrowed only for this synchronous call and is never retained.
|
||||
* @thread Render-thread affine; do not race with rendering or presentation.
|
||||
*/
|
||||
[[nodiscard]] Result<void> compositeConfiguredSurfaceBlend(std::span<const uint8_t> sceneColor);
|
||||
/** @brief Configures Fluid3D's particle-color target blending and depth writes atomically.
|
||||
* @param source Unity BlendMode integer in [0,10].
|
||||
* @param destination Unity BlendMode integer in [0,10].
|
||||
* @param depthWrite When true, only the nearest particle-color fragment at each pixel survives.
|
||||
* @return Success, or invalid argument without changing the prior configuration.
|
||||
* @details Enabling this explicit package path disables the uniform-color shortcut because overlap order
|
||||
* affects the result. It reuses the existing tint and weight arrays and creates no GPU resources or passes.
|
||||
* @ownership No external resource is accepted or retained.
|
||||
* @lifetime The values live with this renderer.
|
||||
* @thread Render-thread affine; do not race configuration with rendering.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureParticleBlend(int source, int destination, bool depthWrite);
|
||||
|
||||
/** @brief Atomically configures thickness reconstruction and surface smoothing.
|
||||
* @param thicknessScale Thickness multiplier in [0,16].
|
||||
* @param thicknessCutoff Fluid3D normalized thickness discard threshold in [0,5]; compares against thickness*10.
|
||||
* @param depthFalloff Bilateral depth falloff in (0,1].
|
||||
* @param smoothIterations Bounded bilateral pass count in [0,8].
|
||||
* @return Success, or a structured error without changing prior configuration.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureSurface(float thicknessScale, float thicknessCutoff, float depthFalloff,
|
||||
int smoothIterations);
|
||||
/** @brief Enables or disables Fluid3D's generateSurface policy for renderConfiguredVolume.
|
||||
* @param enabled True selects existing SSF; false selects existing bounded thickness volume.
|
||||
* @return Success without allocating or changing the current frame.
|
||||
* @details Render-thread affine; the configured value is owned by this renderer.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureSurfaceEnabled(bool enabled);
|
||||
/** @brief Validates and atomically applies every Fluid3DRendererSettings field.
|
||||
* @param settings Owning value borrowed for this render-thread call only.
|
||||
* @return Success, or invalid argument without changing any renderer configuration.
|
||||
* @details Resets existing lazy reduced renderers once after commit; allocates and submits no GPU work.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureRendererSettings(const FluidRendererSettings& settings);
|
||||
/** @brief Returns an owning snapshot of the current Fluid3D renderer settings without allocation. */
|
||||
[[nodiscard]] FluidRendererSettings rendererSettings() const noexcept;
|
||||
/** @brief Sets Fluid3D's world-space surface blur radius for subsequent reconstruction.
|
||||
* @param radius Finite world-space radius in [0,0.1] metres.
|
||||
* @return Success, or invalid argument without changing the prior radius.
|
||||
* @details The radius is converted to a bounded 0-4 pixel kernel per depth sample inside the existing
|
||||
* bilateral smoothing passes. It adds no pass, resource, descriptor or submission. Render-thread affine.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureSurfaceBlurRadius(float radius);
|
||||
/** @brief Atomically configures lighting and material response for subsequent liquid frames.
|
||||
* @param lighting Enables diffuse and specular lighting.
|
||||
* @param smoothness Specular smoothness in [0,1].
|
||||
* @param metalness Reflection tint metalness in [0,1].
|
||||
* @param ambientMultiplier Ambient contribution in [0,6].
|
||||
* @param reflection Fresnel reflection coefficient in [0,1].
|
||||
* @param opacity Thickness-to-alpha coefficient in [0,30].
|
||||
* @return Success, or a structured error without changing prior configuration.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureMaterial(bool lighting, float smoothness, float metalness,
|
||||
float ambientMultiplier, float reflection, float opacity);
|
||||
/** @brief Enables or disables the configured reflection contribution without losing its coefficient.
|
||||
* @param enabled Matches Fluid3D's generateReflection renderer setting.
|
||||
* @return Success. No resource is created and the current output is unchanged until the next render.
|
||||
* @details Render-thread affine. Disabling reflection makes the next shaded frame omit its Fresnel reflection;
|
||||
* enabling it restores the coefficient last supplied to configureMaterial.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureReflection(bool enabled);
|
||||
/** @brief Sets the linear RGB base and reflection colors atomically.
|
||||
* @param baseColor Linear RGB base color with finite channels in [0,1].
|
||||
* @param reflectionColor Linear RGB reflection tint with finite channels in [0,1].
|
||||
* @return Success, or a structured error without changing prior configuration.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureColors(const glm::vec3& baseColor, const glm::vec3& reflectionColor);
|
||||
/** @brief Enables oriented-ellipsoid projection for volume-fluid particles.
|
||||
* @param enabled When true, renderVolume uses particle radii and orientations.
|
||||
* @return Success. GPU resources are created lazily on the next nonempty frame.
|
||||
* @details The disabled path performs no shape copy, anisotropy preprocessing,
|
||||
* extra upload or dispatch. Enabling replaces the spherical splat dispatch; it
|
||||
* does not add a second dispatch. Render-thread affine.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureAnisotropy(bool enabled);
|
||||
/** @brief Configures Fluid3D-style surface target downsampling.
|
||||
* @param factor Integer reconstruction divisor in [1,4].
|
||||
* @return Success, or a structured error without changing the prior factor.
|
||||
* @details Factors above one run splat, smoothing, normals and shading at the
|
||||
* reduced dimensions, then expand outputs in one bounded linear host pass.
|
||||
* Resources are created lazily and retained. Render-thread affine.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureSurfaceDownsample(int factor);
|
||||
/** @brief Sets the independently reconstructed liquid-thickness target divisor.
|
||||
* @param factor Integer divisor in [1,4].
|
||||
* @return Success, or an invalid-argument diagnostic without changing configuration.
|
||||
* @details renderVolume reconstructs thickness at dimensions ceil-divided by factor,
|
||||
* expands it once, then performs material shading. The target
|
||||
* is independent of surfaceDownsample and created lazily. Render-thread affine.
|
||||
* @ownership No external object or pointer is retained.
|
||||
* @lifetime The owned reduced target lives until reconfiguration or renderer destruction.
|
||||
* @thread Render thread only; do not race configuration with rendering.
|
||||
* @reentrancy Must not be called from renderer callbacks.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureThicknessDownsample(int factor);
|
||||
/** @brief Selects perspective or orthographic reconstruction for subsequent frames.
|
||||
* @param orthographic Enables an orthographic camera matching Fluid3D camera setup.
|
||||
* @param verticalHalfSize Positive orthographic world-space half height in [0.001,10000].
|
||||
* @return Success, or invalid argument without changing the prior projection.
|
||||
* @details Render-thread affine. Reuses existing buffers and push constants; no GPU resource is created.
|
||||
*/
|
||||
[[nodiscard]] Result<void> configureProjection(bool orthographic, float verticalHalfSize = 1.f);
|
||||
|
||||
/** @brief Script-friendly wrapper: render(*sim) with a null check.
|
||||
* @param sim Borrowed simulator; null performs no work.
|
||||
* @ownership The simulator is never retained.
|
||||
* @lifetime The borrow lasts only for this call.
|
||||
*/
|
||||
void renderFrom(FluidSimulator* sim);
|
||||
|
||||
int getWidth() const { return params_.width; }
|
||||
@@ -84,10 +406,13 @@ public:
|
||||
const std::vector<uint8_t>& color() const { return color_; }
|
||||
|
||||
/** @return true when the GPU compute path is active. */
|
||||
bool usingGpu() const { return gpuOk_; }
|
||||
bool usingGpu() const { return reducedRenderer_ ? reducedRenderer_->usingGpu() : gpuOk_; }
|
||||
|
||||
/** @brief Switch shading: 0 water, 1 mud. */
|
||||
void setMode(int mode) { params_.mode = mode; }
|
||||
void setMode(int mode) {
|
||||
params_.mode = mode;
|
||||
resetReducedRenderers();
|
||||
}
|
||||
|
||||
/** @brief Update the camera. */
|
||||
void setCamera(const glm::vec3& eye, const glm::vec3& target, const glm::vec3& up, float fovYDeg);
|
||||
@@ -96,38 +421,138 @@ public:
|
||||
void writePpm(const std::string& path) const;
|
||||
|
||||
private:
|
||||
void renderInternal(const std::vector<glm::vec3>& positions, float particleRadius);
|
||||
void renderVolumeInternal(const VolumeFluid& sim, bool colorOnly, float alpha);
|
||||
[[nodiscard]] Result<void> renderVolumeCloud(const VolumeFluid& sim, float absorption, bool gasOnly);
|
||||
void renderCpu();
|
||||
bool ensureGpu();
|
||||
bool ensureMulticolorGpu();
|
||||
bool ensureAnisotropyGpu();
|
||||
void releaseGpu() noexcept;
|
||||
void uploadParticles();
|
||||
void buildAnisotropicSplats();
|
||||
void setCommonConstants(gpgpu::ComputeShader* shader, float falloff);
|
||||
void applyConfiguredShading();
|
||||
void refreshCustomShadingFlag();
|
||||
void ensureReducedRenderer();
|
||||
void expandReducedOutputs(bool colorOnly);
|
||||
void ensureThicknessRenderer();
|
||||
void replaceThicknessFromReduced();
|
||||
void resetReducedRenderers();
|
||||
static glm::vec4 blendFluidColor(FluidBlendFactor factor, const glm::vec4& source, const glm::vec4& destination);
|
||||
|
||||
FluidSurfaceParams params_;
|
||||
bool preferGpu_ = false;
|
||||
bool gpuOk_ = false;
|
||||
/** @brief Prevents repeated resource creation after an unavailable or failed GPU attempt. */
|
||||
bool gpuAttempted_ = false;
|
||||
bool gpuOk_ = false;
|
||||
|
||||
std::vector<glm::vec3> positions_;
|
||||
/** @brief Reused volume-fluid particle colors and per-pixel tint accumulation. */
|
||||
std::vector<glm::vec4> volumeColors_;
|
||||
/** @brief Uniform volume tint consumed directly by the GPU shade pass when present. */
|
||||
glm::vec4 uniformVolumeColor_{0.f};
|
||||
bool uniformVolumeGpuShade_ = false;
|
||||
bool multicolorVolumeGpuShade_ = false;
|
||||
bool multicolorGpuReady_ = false;
|
||||
/** @brief Whether host depth belongs to the current color frame. */
|
||||
bool auxiliaryCurrent_ = false;
|
||||
/** @brief Current RGBA8 output is still authoritative in bufColor_. */
|
||||
bool residentColorCurrent_ = false;
|
||||
/** @brief One-call hint allowing the Vulkan color download to be omitted. */
|
||||
bool skipHostColorReadback_ = false;
|
||||
bool customShading_ = false;
|
||||
bool anisotropyEnabled_ = false;
|
||||
bool anisotropicFrame_ = false;
|
||||
int surfaceDownsample_ = 1;
|
||||
int thicknessDownsample_ = 1;
|
||||
std::unique_ptr<FluidSurfaceRenderer> reducedRenderer_;
|
||||
std::unique_ptr<FluidSurfaceRenderer> thicknessRenderer_;
|
||||
float thicknessCutoff_ = 0.f;
|
||||
bool surfaceEnabled_ = true;
|
||||
/** @brief Negative retains the established fixed 5x5 native kernel until explicitly configured. */
|
||||
float surfaceBlurRadius_ = -1.f;
|
||||
bool lighting_ = true;
|
||||
float smoothness_ = .8f;
|
||||
float metalness_ = 0.f;
|
||||
float ambientMultiplier_ = .55f;
|
||||
float reflection_ = .75f;
|
||||
bool reflectionEnabled_ = true;
|
||||
float opacity_ = .35f;
|
||||
glm::vec3 baseColor_{.05f, .32f, .72f};
|
||||
glm::vec3 reflectionColor_{.55f, .72f, 1.f};
|
||||
float refractionTransparency_ = 1.f;
|
||||
bool refractionEnabled_ = true;
|
||||
float refractionAbsorption_ = 5.f;
|
||||
float refractionCoefficient_ = .01f;
|
||||
int refractionDownsample_ = 1;
|
||||
FluidBlendFactor surfaceBlendSource_ = FluidBlendFactor::SourceAlpha;
|
||||
FluidBlendFactor surfaceBlendDestination_ = FluidBlendFactor::OneMinusSourceAlpha;
|
||||
FluidBlendFactor particleBlendSource_ = FluidBlendFactor::DestinationColor;
|
||||
FluidBlendFactor particleBlendDestination_ = FluidBlendFactor::Zero;
|
||||
bool particleDepthWrite_ = false;
|
||||
bool particleBlendConfigured_ = false;
|
||||
/** @brief Reused downsampled opaque scene for configured refraction. */
|
||||
std::vector<uint8_t> refractionScratch_;
|
||||
std::vector<glm::vec4> volumeTint_;
|
||||
std::vector<float> volumeTintWeights_;
|
||||
/** @brief Reused premultiplied gas tint and separable-blur scratch. */
|
||||
std::vector<glm::vec4> gasTintScratch_;
|
||||
std::vector<float> gasDensityScratch_;
|
||||
struct GasSplat {
|
||||
float cx = 0.f, cy = 0.f, r = 0.f, z = 0.f;
|
||||
int x0 = 0, x1 = -1, y0 = 0, y1 = -1;
|
||||
};
|
||||
std::vector<GasSplat> gasSplats_;
|
||||
/** @brief Reused packed GPU upload and normal readback storage. */
|
||||
std::vector<float> gpuParticles_;
|
||||
/** @brief Reused 3-vec4 projected ellipsoid upload, populated only when enabled. */
|
||||
std::vector<float> anisotropicSplats_;
|
||||
std::vector<glm::vec3> particleRadii_;
|
||||
std::vector<glm::vec4> particleOrientations_;
|
||||
std::vector<glm::vec4> gpuNormals_;
|
||||
std::vector<float> depth_;
|
||||
/** @brief Reused CPU bilateral-filter ping-pong storage. */
|
||||
std::vector<float> depthScratch_;
|
||||
std::vector<float> thickness_;
|
||||
std::vector<glm::vec3> normals_;
|
||||
std::vector<uint8_t> color_;
|
||||
/** @brief Reused CPU handoff storage for diffuse position.xyz/life. */
|
||||
std::vector<glm::vec4> diffuseParticles_;
|
||||
bool foamEnabled_ = true;
|
||||
int foamDownsample_ = 1;
|
||||
/** @brief Reused reduced foam coverage target; empty for the full-resolution path. */
|
||||
std::vector<float> foamCoverageScratch_;
|
||||
struct DiffuseSplat {
|
||||
float x = 0.f, y = 0.f, r = 0.f, z = 0.f, alpha = 0.f;
|
||||
int x0 = 0, x1 = -1, y0 = 0, y1 = -1;
|
||||
};
|
||||
/** @brief Reused preflight/projected splats; content is derived frame scratch. */
|
||||
std::vector<DiffuseSplat> diffuseSplats_;
|
||||
|
||||
eve::gpgpu::Gpgpu* gpgpu_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shClear_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shSplat_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shSmooth_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shNormal_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shShade_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufParts_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufDepthA_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufDepthB_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufThick_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufNormal_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufColor_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* stDepth_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* stThick_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* stNormal_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* stColor_ = nullptr;
|
||||
eve::gpgpu::Sequence* seq_ = nullptr;
|
||||
eve::gpgpu::Gpgpu* gpgpu_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shClear_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shSplat_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shAnisotropicSplat_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shSmooth_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shNormal_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shShade_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shColorClear_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shColorSplat_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufParts_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufAnisotropicParts_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufDepthA_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufDepthB_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufThick_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufNormal_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufColor_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufParticleColors_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufColorAccum_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* stDepth_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* stThick_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* stNormal_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* stColor_ = nullptr;
|
||||
eve::gpgpu::Sequence* seq_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace eve::fluids
|
||||
@@ -1,14 +1,21 @@
|
||||
#include "fluids/Fluids.h"
|
||||
|
||||
#include "common/SquirrelBinding.h"
|
||||
#include "fluids/FluidGpuKernels.h"
|
||||
#include "fluids/VolumeFluid.h"
|
||||
#include "fluids/VolumeFluidBindings.h"
|
||||
#include "fluids/VolumeFluidCodec.h"
|
||||
#include "fluids/VolumeFluidDiffuse.h"
|
||||
#include "gpgpu/ComputeShader.h"
|
||||
#include "gpgpu/Gpgpu.h"
|
||||
#include "gpgpu/GpuBuffer.h"
|
||||
#include "gpgpu/Sequence.h"
|
||||
#include "image/ImageData.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <simplesquirrel/simplesquirrel.hpp>
|
||||
#include <utility>
|
||||
@@ -58,6 +65,7 @@ FluidSimulator::FluidSimulator(int maxParticles, const FluidParams& params, bool
|
||||
|
||||
FluidSimulator::~FluidSimulator() {
|
||||
delete seq_;
|
||||
delete shAdvance_;
|
||||
delete shIntegrate_;
|
||||
delete shApply_;
|
||||
delete shDelta_;
|
||||
@@ -84,6 +92,7 @@ void FluidSimulator::setSdf(const MeshSdf& sdf) {
|
||||
if (gpuOk_) {
|
||||
// Grid and SDF buffers depend on the field; rebuild them.
|
||||
delete seq_;
|
||||
delete shAdvance_;
|
||||
delete shIntegrate_;
|
||||
delete shApply_;
|
||||
delete shDelta_;
|
||||
@@ -101,7 +110,7 @@ void FluidSimulator::setSdf(const MeshSdf& sdf) {
|
||||
delete bufHead_;
|
||||
delete bufVel_;
|
||||
delete bufPos_;
|
||||
shClear_ = shBuild_ = shDensityLambda_ = shDelta_ = shApply_ = shIntegrate_ = nullptr;
|
||||
shClear_ = shBuild_ = shDensityLambda_ = shDelta_ = shApply_ = shIntegrate_ = shAdvance_ = nullptr;
|
||||
bufPos_ = bufVel_ = bufHead_ = bufNext_ = bufDens_ = bufLambda_ = bufGrad_ = bufSdf_ = nullptr;
|
||||
stagePos_ = stageVel_ = stageDens_ = nullptr;
|
||||
seq_ = nullptr;
|
||||
@@ -137,6 +146,8 @@ void FluidSimulator::stepSolver(float dt, int substeps) {
|
||||
seq_->recordDispatch(shBuild_, groupsFor(sim_.maxParticles()));
|
||||
setCommonConstants(shIntegrate_, sub);
|
||||
seq_->recordDispatch(shIntegrate_, groupsFor(sim_.maxParticles()));
|
||||
setCommonConstants(shAdvance_, sub);
|
||||
seq_->recordDispatch(shAdvance_, groupsFor(sim_.maxParticles()));
|
||||
for (int k = 0; k < pbf; ++k) {
|
||||
setCommonConstants(shClear_, sub);
|
||||
seq_->recordDispatch(shClear_, groupsFor(grid_.cellCount()));
|
||||
@@ -259,6 +270,7 @@ bool FluidSimulator::ensureGpu() {
|
||||
shDelta_ = gpgpu_->newShader(kFluidComputeDelta);
|
||||
shApply_ = gpgpu_->newShader(kFluidApplyDelta);
|
||||
shIntegrate_ = gpgpu_->newShader(kFluidIntegrate);
|
||||
shAdvance_ = gpgpu_->newShader(kFluidApplyDelta);
|
||||
bufPos_ = gpgpu_->newBuffer(max * 4 * int(sizeof(float)), "storage");
|
||||
bufVel_ = gpgpu_->newBuffer(max * 4 * int(sizeof(float)), "storage");
|
||||
bufHead_ = gpgpu_->newBuffer(grid_.cellCount() * int(sizeof(int)), "storage");
|
||||
@@ -294,6 +306,7 @@ bool FluidSimulator::ensureGpu() {
|
||||
shDelta_->bindBuffer(6, bufLambda_);
|
||||
shDelta_->bindBuffer(7, bufGrad_);
|
||||
shApply_->bindBuffer(0, bufPos_);
|
||||
shApply_->bindBuffer(1, bufVel_);
|
||||
shApply_->bindBuffer(2, bufHead_);
|
||||
shApply_->bindBuffer(3, bufNext_);
|
||||
shApply_->bindBuffer(7, bufGrad_);
|
||||
@@ -303,6 +316,11 @@ bool FluidSimulator::ensureGpu() {
|
||||
shIntegrate_->bindBuffer(2, bufHead_);
|
||||
shIntegrate_->bindBuffer(3, bufNext_);
|
||||
shIntegrate_->bindBuffer(5, bufSdf_);
|
||||
shIntegrate_->bindBuffer(7, bufGrad_);
|
||||
shAdvance_->bindBuffer(0, bufPos_);
|
||||
shAdvance_->bindBuffer(1, bufVel_);
|
||||
shAdvance_->bindBuffer(5, bufSdf_);
|
||||
shAdvance_->bindBuffer(7, bufGrad_);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -373,6 +391,7 @@ void FluidSimulator::setCommonConstants(gpgpu::ComputeShader* shader, float dt)
|
||||
shader->setFloat(kPushSdfCell, sim_.sdf().cellSize);
|
||||
shader->setFloat(kPushIterations, float(p.iterations));
|
||||
shader->setFloat(kPushMode, 0.f);
|
||||
if (shader == shAdvance_) shader->setFloat(kPushMode, 1.f);
|
||||
shader->setFloat(kPushTime, 0.f);
|
||||
shader->setFloat(kPushPbf, float(p.pbfIterations));
|
||||
}
|
||||
@@ -393,8 +412,8 @@ int Fluids::getSimulatorCount() const { return int(simulators_.size()); }
|
||||
|
||||
FluidSurfaceRenderer* Fluids::newSurfaceRenderer(int width, int height) {
|
||||
FluidSurfaceParams params;
|
||||
params.width = std::max(8, width);
|
||||
params.height = std::max(8, height);
|
||||
params.width = std::clamp(width, 8, 1024);
|
||||
params.height = std::clamp(height, 8, 1024);
|
||||
auto r = std::make_unique<FluidSurfaceRenderer>(params, true);
|
||||
FluidSurfaceRenderer* raw = r.get();
|
||||
renderers_.push_back(std::move(r));
|
||||
@@ -406,6 +425,11 @@ int Fluids::getRendererCount() const { return int(renderers_.size()); }
|
||||
void Fluids::expose(ssq::Table& table) {
|
||||
auto cls = table.addClass(name, Fluids::create, false);
|
||||
expose(cls);
|
||||
cls.addFunc("fluidRendererSettingsDefaults", [vm = table.getHandle()](Fluids*) {
|
||||
return eve::script::projectStatusResult(vm, eve::Status::success(), true, true,
|
||||
encodeFluidRendererSettings(FluidRendererSettings{}));
|
||||
});
|
||||
exposeVolumeFluidType(table);
|
||||
|
||||
auto sim = table.addClass<FluidSimulator>(
|
||||
"FluidSim", std::function<FluidSimulator*()>([]() -> FluidSimulator* { return nullptr; }), true);
|
||||
@@ -428,15 +452,196 @@ void Fluids::expose(ssq::Table& table) {
|
||||
"FluidSurface", std::function<FluidSurfaceRenderer*()>([]() -> FluidSurfaceRenderer* { return nullptr; }),
|
||||
true);
|
||||
surf.addFunc("render", &FluidSurfaceRenderer::renderFrom);
|
||||
surf.addFunc("renderVolume",
|
||||
[](FluidSurfaceRenderer* renderer, VolumeFluid* solver) { renderer->renderVolume(*solver); });
|
||||
surf.addFunc("renderVolumeInterpolated", [vm = table.getHandle()](FluidSurfaceRenderer* renderer,
|
||||
VolumeFluid* solver, float alpha) {
|
||||
if (!solver)
|
||||
return eve::script::projectResult(
|
||||
vm, eve::Result<void>::failure(eve::Diagnostic::error(
|
||||
eve::DiagnosticCode::InvalidArgument, "Missing volume fluid", "fluids.surface.interpolation")));
|
||||
return eve::script::projectResult(vm, renderer->renderVolumeInterpolated(*solver, alpha));
|
||||
});
|
||||
surf.addFunc("renderVolumeColorOnly",
|
||||
[](FluidSurfaceRenderer* renderer, VolumeFluid* solver) { renderer->renderVolumeColorOnly(*solver); });
|
||||
surf.addFunc("renderGasVolume", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, VolumeFluid* solver,
|
||||
float absorption) {
|
||||
if (!solver)
|
||||
return eve::script::projectResult(
|
||||
vm, eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument,
|
||||
"Missing volume fluid", "fluids.surface.gas")));
|
||||
return eve::script::projectResult(vm, renderer->renderGasVolume(*solver, absorption));
|
||||
});
|
||||
surf.addFunc("renderVolumeWithoutSurface", [vm = table.getHandle()](FluidSurfaceRenderer* renderer,
|
||||
VolumeFluid* solver, float absorption) {
|
||||
if (!solver)
|
||||
return eve::script::projectResult(
|
||||
vm, eve::Result<void>::failure(eve::Diagnostic::error(
|
||||
eve::DiagnosticCode::InvalidArgument, "Missing volume fluid", "fluids.surface.volume-cloud")));
|
||||
return eve::script::projectResult(vm, renderer->renderVolumeWithoutSurface(*solver, absorption));
|
||||
});
|
||||
surf.addFunc(
|
||||
"renderConfiguredVolume", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, VolumeFluid* solver) {
|
||||
if (!solver)
|
||||
return eve::script::projectResult(vm, eve::Result<void>::failure(eve::Diagnostic::error(
|
||||
eve::DiagnosticCode::InvalidArgument, "Missing volume fluid",
|
||||
"fluids.surface.configured-volume")));
|
||||
return eve::script::projectResult(vm, renderer->renderConfiguredVolume(*solver));
|
||||
});
|
||||
surf.addFunc("prepare", [vm = table.getHandle()](FluidSurfaceRenderer* renderer) {
|
||||
return eve::script::projectResult(vm, renderer->prepare());
|
||||
});
|
||||
surf.addFunc("copyToTexture", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, graphics::Graphics* graphics,
|
||||
graphics::Texture* texture) {
|
||||
return eve::script::projectResult(vm, renderer->copyToTexture(graphics, texture));
|
||||
});
|
||||
surf.addFunc("renderVolumeColorToTexture", [vm = table.getHandle()](
|
||||
FluidSurfaceRenderer* renderer, VolumeFluid* solver,
|
||||
graphics::Graphics* graphics, graphics::Texture* texture) {
|
||||
if (!solver)
|
||||
return eve::script::projectResult(vm, eve::Result<void>::failure(eve::Diagnostic::error(
|
||||
eve::DiagnosticCode::InvalidArgument, "Missing volume fluid",
|
||||
"fluids.surface.renderVolumeColorToTexture")));
|
||||
return eve::script::projectResult(vm, renderer->renderVolumeColorToTexture(*solver, graphics, texture));
|
||||
});
|
||||
surf.addFunc("compositeDiffuse", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, VolumeFluidDiffuse* pool,
|
||||
float radius, float opacity, float fadeSeconds) {
|
||||
if (!pool)
|
||||
return eve::script::projectResult(
|
||||
vm, eve::Result<void>::failure(eve::Diagnostic::error(
|
||||
eve::DiagnosticCode::InvalidArgument, "Missing diffuse pool", "fluids.surface.diffuse")));
|
||||
return eve::script::projectResult(vm, renderer->compositeDiffuse(*pool, radius, opacity, fadeSeconds));
|
||||
});
|
||||
surf.addFunc("configureFoam",
|
||||
[vm = table.getHandle()](FluidSurfaceRenderer* renderer, bool enabled, int downsample) {
|
||||
return eve::script::projectResult(vm, renderer->configureFoam(enabled, downsample));
|
||||
});
|
||||
surf.addFunc("compositeSceneRefraction", [vm = table.getHandle()](FluidSurfaceRenderer* renderer,
|
||||
image::ImageData* scene, float distortion,
|
||||
float absorption) {
|
||||
if (!scene || scene->getFormat() != "RGBA8" || scene->getWidth() != renderer->getWidth() ||
|
||||
scene->getHeight() != renderer->getHeight())
|
||||
return eve::script::projectResult(
|
||||
vm, eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument,
|
||||
"Refraction requires matching RGBA8 scene image",
|
||||
"fluids.surface.refraction")));
|
||||
return eve::script::projectResult(
|
||||
vm, renderer->compositeSceneRefraction(
|
||||
std::span<const uint8_t>(static_cast<const uint8_t*>(scene->getData()), scene->getSize()),
|
||||
distortion, absorption));
|
||||
});
|
||||
surf.addFunc("configureRefraction", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, float transparency,
|
||||
float absorption, float coefficient, int downsample) {
|
||||
return eve::script::projectResult(
|
||||
vm, renderer->configureRefraction(transparency, absorption, coefficient, downsample));
|
||||
});
|
||||
surf.addFunc("configureRefractionEnabled", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, bool enabled) {
|
||||
return eve::script::projectResult(vm, renderer->configureRefractionEnabled(enabled));
|
||||
});
|
||||
surf.addFunc("compositeConfiguredSceneRefraction", [vm = table.getHandle()](FluidSurfaceRenderer* renderer,
|
||||
image::ImageData* scene) {
|
||||
if (!scene || scene->getFormat() != "RGBA8" || scene->getWidth() != renderer->getWidth() ||
|
||||
scene->getHeight() != renderer->getHeight())
|
||||
return eve::script::projectResult(vm, eve::Result<void>::failure(eve::Diagnostic::error(
|
||||
eve::DiagnosticCode::InvalidArgument,
|
||||
"Configured refraction requires matching RGBA8 scene image",
|
||||
"fluids.surface.configuredRefraction")));
|
||||
return eve::script::projectResult(vm, renderer->compositeConfiguredSceneRefraction(std::span<const uint8_t>(
|
||||
static_cast<const uint8_t*>(scene->getData()), scene->getSize())));
|
||||
});
|
||||
surf.addFunc("configureSurfaceBlend",
|
||||
[vm = table.getHandle()](FluidSurfaceRenderer* renderer, int source, int destination) {
|
||||
return eve::script::projectResult(vm, renderer->configureSurfaceBlend(source, destination));
|
||||
});
|
||||
surf.addFunc("configureParticleBlend", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, int source,
|
||||
int destination, bool depthWrite) {
|
||||
return eve::script::projectResult(vm, renderer->configureParticleBlend(source, destination, depthWrite));
|
||||
});
|
||||
surf.addFunc("compositeConfiguredSurfaceBlend", [vm = table.getHandle()](FluidSurfaceRenderer* renderer,
|
||||
image::ImageData* scene) {
|
||||
if (!scene || scene->getFormat() != "RGBA8" || scene->getWidth() != renderer->getWidth() ||
|
||||
scene->getHeight() != renderer->getHeight())
|
||||
return eve::script::projectResult(
|
||||
vm, eve::Result<void>::failure(eve::Diagnostic::error(
|
||||
eve::DiagnosticCode::InvalidArgument,
|
||||
"Configured surface blend requires matching RGBA8 scene image", "fluids.surface.blend")));
|
||||
return eve::script::projectResult(vm, renderer->compositeConfiguredSurfaceBlend(std::span<const uint8_t>(
|
||||
static_cast<const uint8_t*>(scene->getData()), scene->getSize())));
|
||||
});
|
||||
surf.addFunc("setMode", &FluidSurfaceRenderer::setMode);
|
||||
surf.addFunc(
|
||||
"configureSurface", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, float thicknessScale,
|
||||
float thicknessCutoff, float depthFalloff, int smoothIterations) {
|
||||
return eve::script::projectResult(
|
||||
vm, renderer->configureSurface(thicknessScale, thicknessCutoff, depthFalloff, smoothIterations));
|
||||
});
|
||||
surf.addFunc("configureSurfaceEnabled", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, bool enabled) {
|
||||
return eve::script::projectResult(vm, renderer->configureSurfaceEnabled(enabled));
|
||||
});
|
||||
surf.addFunc("configureRendererSettings",
|
||||
[vm = table.getHandle()](FluidSurfaceRenderer* renderer, ssq::Object object) {
|
||||
auto value = eve::script::valueFromSquirrel(object);
|
||||
if (!value) return eve::script::projectResult(vm, eve::Result<void>::failure(value.status()));
|
||||
auto settings = decodeFluidRendererSettings(value.value());
|
||||
if (!settings)
|
||||
return eve::script::projectResult(vm, eve::Result<void>::failure(settings.status()));
|
||||
return eve::script::projectResult(vm, renderer->configureRendererSettings(settings.value()));
|
||||
});
|
||||
surf.addFunc("rendererSettings", [vm = table.getHandle()](FluidSurfaceRenderer* renderer) {
|
||||
return eve::script::projectStatusResult(vm, eve::Status::success(), true, true,
|
||||
encodeFluidRendererSettings(renderer->rendererSettings()));
|
||||
});
|
||||
surf.addFunc("configureSurfaceBlurRadius", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, float radius) {
|
||||
return eve::script::projectResult(vm, renderer->configureSurfaceBlurRadius(radius));
|
||||
});
|
||||
surf.addFunc("configureMaterial", [vm = table.getHandle()](
|
||||
FluidSurfaceRenderer* renderer, bool lighting, float smoothness,
|
||||
float metalness, float ambientMultiplier, float reflection, float opacity) {
|
||||
return eve::script::projectResult(
|
||||
vm, renderer->configureMaterial(lighting, smoothness, metalness, ambientMultiplier, reflection, opacity));
|
||||
});
|
||||
surf.addFunc("configureReflection", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, bool enabled) {
|
||||
return eve::script::projectResult(vm, renderer->configureReflection(enabled));
|
||||
});
|
||||
surf.addFunc("configureColors", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, float br, float bg,
|
||||
float bb, float rr, float rg, float rb) {
|
||||
return eve::script::projectResult(vm, renderer->configureColors({br, bg, bb}, {rr, rg, rb}));
|
||||
});
|
||||
surf.addFunc("configureAnisotropy", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, bool enabled) {
|
||||
return eve::script::projectResult(vm, renderer->configureAnisotropy(enabled));
|
||||
});
|
||||
surf.addFunc("configureSurfaceDownsample", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, int factor) {
|
||||
return eve::script::projectResult(vm, renderer->configureSurfaceDownsample(factor));
|
||||
});
|
||||
surf.addFunc("configureThicknessDownsample", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, int factor) {
|
||||
return eve::script::projectResult(vm, renderer->configureThicknessDownsample(factor));
|
||||
});
|
||||
surf.addFunc("configureProjection", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, bool orthographic,
|
||||
float verticalHalfSize) {
|
||||
return eve::script::projectResult(vm, renderer->configureProjection(orthographic, verticalHalfSize));
|
||||
});
|
||||
surf.addFunc("setCamera", &FluidSurfaceRenderer::setCamera);
|
||||
surf.addFunc("setCameraXYZ",
|
||||
[](FluidSurfaceRenderer* renderer, float ex, float ey, float ez, float tx, float ty, float tz,
|
||||
float fov) { renderer->setCamera({ex, ey, ez}, {tx, ty, tz}, {0.f, 1.f, 0.f}, fov); });
|
||||
surf.addFunc("getWidth", &FluidSurfaceRenderer::getWidth);
|
||||
surf.addFunc("getHeight", &FluidSurfaceRenderer::getHeight);
|
||||
surf.addFunc("usingGpu", &FluidSurfaceRenderer::usingGpu);
|
||||
surf.addFunc("writePpm", &FluidSurfaceRenderer::writePpm);
|
||||
surf.addFunc("copyToImage", [vm = table.getHandle()](FluidSurfaceRenderer* renderer, image::ImageData* image) {
|
||||
if (!image || image->getFormat() != "RGBA8" || image->getWidth() != renderer->getWidth() ||
|
||||
image->getHeight() != renderer->getHeight() || image->getSize() != renderer->color().size())
|
||||
return eve::script::projectResult(
|
||||
vm, eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument,
|
||||
"Fluid output requires matching RGBA8 image",
|
||||
"fluids.copyToImage")));
|
||||
std::memcpy(image->getData(), renderer->color().data(), renderer->color().size());
|
||||
return eve::script::projectResult(vm, eve::Result<void>::success());
|
||||
});
|
||||
}
|
||||
|
||||
void Fluids::expose(ssq::Class& cls) {
|
||||
exposeVolumeFluidFactory(cls);
|
||||
cls.addFunc("getName", &Fluids::getName);
|
||||
cls.addFunc("newSimulator", &Fluids::newSimulator);
|
||||
cls.addFunc("getSimulatorCount", &Fluids::getSimulatorCount);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "fluids/SurfaceFluidRenderData.h"
|
||||
#include "fluids/SurfaceFluidSceneRenderer.h"
|
||||
#include "fluids/SurfaceWetnessField.h"
|
||||
#include "fluids/VolumeFluidShadowCaster.h"
|
||||
#include "physics/backend/SimulationBackend.h"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
@@ -172,6 +173,7 @@ private:
|
||||
eve::gpgpu::ComputeShader* shDelta_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shApply_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shIntegrate_ = nullptr;
|
||||
eve::gpgpu::ComputeShader* shAdvance_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufPos_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufVel_ = nullptr;
|
||||
eve::gpgpu::GpuBuffer* bufHead_ = nullptr;
|
||||
@@ -207,6 +209,12 @@ public:
|
||||
* @param height target height in pixels.
|
||||
* @return renderer owned by the module.
|
||||
*/
|
||||
/**
|
||||
* @brief Creates the shared screen-space fluid renderer.
|
||||
* @details Dimensions are clamped to [8,1024]
|
||||
* to bound CPU scratch storage,
|
||||
* Vulkan buffers and synchronous readback cost. Owned by this module.
|
||||
*/
|
||||
FluidSurfaceRenderer* newSurfaceRenderer(int width = 160, int height = 160);
|
||||
|
||||
/** @return number of live simulators owned by the module. */
|
||||
|
||||
@@ -155,6 +155,11 @@ eve::Result<void> ScreenSpaceSurfaceReconstructionAdapter::reconstruct(std::span
|
||||
return applied();
|
||||
}
|
||||
|
||||
eve::Result<void> ScreenSpaceSurfaceReconstructionAdapter::occlude(std::span<const float> sceneDepth, float depthBias) {
|
||||
if (!renderer_) return staleSolver("fluids.screenSpaceReconstruction.renderer");
|
||||
return renderer_->occludeWithSceneDepth(sceneDepth, depthBias);
|
||||
}
|
||||
|
||||
int ScreenSpaceSurfaceReconstructionAdapter::width() const noexcept { return renderer_ ? renderer_->getWidth() : 0; }
|
||||
|
||||
int ScreenSpaceSurfaceReconstructionAdapter::height() const noexcept { return renderer_ ? renderer_->getHeight() : 0; }
|
||||
|
||||
@@ -95,6 +95,10 @@ public:
|
||||
[[nodiscard("check screen-space reconstruction")]]
|
||||
virtual eve::Result<void> reconstruct(std::span<const glm::vec3> positions, float particleRadius) = 0;
|
||||
|
||||
/** @brief Removes reconstructed color behind borrowed linear scene depth without allocating. */
|
||||
[[nodiscard("check scene-depth composition")]]
|
||||
virtual eve::Result<void> occlude(std::span<const float> sceneDepth, float depthBias = 0.001f) = 0;
|
||||
|
||||
/** @brief Output width in pixels. */
|
||||
[[nodiscard]] virtual int width() const noexcept = 0;
|
||||
/** @brief Output height in pixels. */
|
||||
@@ -169,6 +173,7 @@ public:
|
||||
ScreenSpaceSurfaceReconstructionAdapter& operator=(const ScreenSpaceSurfaceReconstructionAdapter&) = delete;
|
||||
|
||||
[[nodiscard]] eve::Result<void> reconstruct(std::span<const glm::vec3> positions, float particleRadius) override;
|
||||
[[nodiscard]] eve::Result<void> occlude(std::span<const float> sceneDepth, float depthBias = 0.001f) override;
|
||||
[[nodiscard]] int width() const noexcept override;
|
||||
[[nodiscard]] int height() const noexcept override;
|
||||
[[nodiscard]] std::span<const float> depth() const noexcept override;
|
||||
|
||||
@@ -0,0 +1,877 @@
|
||||
#include "fluids/VolumeFluidInternal.inc"
|
||||
|
||||
namespace eve::fluids {
|
||||
VolumeFluid::VolumeFluid(const VolumeFluidSettings& settings) : impl_(std::make_unique<Impl>(settings)) {}
|
||||
VolumeFluid::~VolumeFluid() = default;
|
||||
|
||||
Result<std::unique_ptr<VolumeFluid>> VolumeFluid::create(const VolumeFluidSettings& s) {
|
||||
const auto extent = s.maximum - s.minimum;
|
||||
const bool valid = s.capacity > 0 && s.capacity <= 1000000 && std::isfinite(s.spacing) && s.spacing >= 0.001f &&
|
||||
s.spacing <= 10.f && finite(s.gravity) && glm::length(s.gravity) <= 1000.f &&
|
||||
finite(s.minimum) && finite(s.maximum) &&
|
||||
glm::all(glm::greaterThan(extent, glm::vec3(s.spacing * 2.f))) && s.iterations > 0 &&
|
||||
s.iterations <= 20;
|
||||
if (!valid)
|
||||
return Result<std::unique_ptr<VolumeFluid>>::failure(Diagnostic::error(
|
||||
DiagnosticCode::InvalidArgument, "Invalid volume-fluid settings", "fluids.volume.settings"));
|
||||
const auto cells = glm::ceil(glm::dvec3(extent) / double(2.f * s.spacing)) + 1.0;
|
||||
if (cells.x * cells.y * cells.z > 2000000.0)
|
||||
return Result<std::unique_ptr<VolumeFluid>>::failure(Diagnostic::error(
|
||||
DiagnosticCode::InvalidArgument, "Volume-fluid grid exceeds 2000000 cells", "fluids.volume.bounds"));
|
||||
return Result<std::unique_ptr<VolumeFluid>>::success(std::unique_ptr<VolumeFluid>(new VolumeFluid(s)));
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::emit(std::span<const VolumeFluidParticle> input) {
|
||||
if (input.size() > impl_->settings.capacity - impl_->state.size()) return invalid("Particle capacity exceeded");
|
||||
if (!impl_->colliders.empty() && uint64_t(impl_->state.size() + input.size()) * impl_->colliders.size() > 4000000)
|
||||
return invalid("Analytic collider candidate budget exceeded");
|
||||
if (!impl_->sdfColliders.empty() &&
|
||||
uint64_t(impl_->state.size() + input.size()) * impl_->sdfColliders.size() > 4000000)
|
||||
return invalid("SDF collider candidate budget exceeded");
|
||||
if (!impl_->heightFieldColliders.empty() &&
|
||||
uint64_t(impl_->state.size() + input.size()) * impl_->heightFieldColliders.size() > 4000000)
|
||||
return invalid("Height-field collider candidate budget exceeded");
|
||||
for (const auto& p : input) {
|
||||
if (!finite(p.position) || !finite(p.velocity) || !finite(p.angularVelocity) || !finite(p.color) ||
|
||||
!finite(p.data) || !validMaterial(p.material) || !validParticleShape(p) || !range(p.life, 0.f, 86400.f) ||
|
||||
!validFilter(p.collisionFilter) || p.actorGroup > 0x00ffffffu || glm::length(p.velocity) > 100.f ||
|
||||
glm::length(p.angularVelocity) > 1000.f ||
|
||||
glm::any(glm::notEqual(impl_->constrain(p.position), p.position)))
|
||||
return invalid("Invalid emitted particle");
|
||||
}
|
||||
const size_t first = impl_->state.size();
|
||||
impl_->state.insert(impl_->state.end(), input.begin(), input.end());
|
||||
for (size_t i = first; i < impl_->state.size(); ++i) {
|
||||
auto& particle = impl_->state[i];
|
||||
if (glm::all(glm::equal(particle.radii, glm::vec3(0.f))))
|
||||
particle.radii = glm::vec3(impl_->settings.spacing * .5f);
|
||||
particle.orientation = glm::normalize(particle.orientation);
|
||||
impl_->renderPrevious.push_back(particle.position);
|
||||
impl_->grabberLabels.push_back(Impl::noGrabber);
|
||||
impl_->grabberLocalPositions.push_back(glm::vec3(0.f));
|
||||
impl_->recordParticleEvent(VolumeFluidParticleEventType::Emitted, unsigned(i), particle);
|
||||
}
|
||||
impl_->winds.clear();
|
||||
impl_->externalForces.clear();
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::configureParticleEvents(unsigned capacity) {
|
||||
if (capacity > 65536) return invalid("Particle event capacity exceeds 65536");
|
||||
std::vector<VolumeFluidParticleEvent> replacement;
|
||||
replacement.reserve(capacity);
|
||||
impl_->particleEvents.swap(replacement);
|
||||
impl_->particleEventCapacity = capacity;
|
||||
impl_->droppedParticleEvents = 0;
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
VolumeFluidParticleEventBatch VolumeFluid::drainParticleEvents() {
|
||||
VolumeFluidParticleEventBatch batch;
|
||||
batch.events = impl_->particleEvents;
|
||||
batch.dropped = impl_->droppedParticleEvents;
|
||||
impl_->particleEvents.clear();
|
||||
impl_->droppedParticleEvents = 0;
|
||||
return batch;
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::step(float seconds, unsigned substeps) {
|
||||
return stepWithThermalContacts(seconds, substeps, {});
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::setParticleWinds(std::span<const glm::vec3> winds) {
|
||||
if (winds.empty()) {
|
||||
impl_->winds.clear();
|
||||
return Result<void>::success();
|
||||
}
|
||||
if (winds.size() != impl_->state.size() || std::any_of(winds.begin(), winds.end(), [](glm::vec3 wind) {
|
||||
return !finite(wind) || glm::length(wind) > 1000.f;
|
||||
}))
|
||||
return invalid("Invalid per-particle wind count or velocity");
|
||||
impl_->winds.assign(winds.begin(), winds.end());
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::setParticleExternalForces(std::span<const glm::vec3> forces) {
|
||||
if (forces.empty()) {
|
||||
impl_->externalForces.clear();
|
||||
return Result<void>::success();
|
||||
}
|
||||
if (forces.size() != impl_->state.size() || std::any_of(forces.begin(), forces.end(), [](glm::vec3 force) {
|
||||
return !finite(force) || glm::length(force) > 1000.f;
|
||||
}))
|
||||
return invalid("Invalid per-particle external force count or magnitude");
|
||||
impl_->externalForces.assign(forces.begin(), forces.end());
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::accumulateWindZones(std::span<const VolumeFluidWindZone> zones, float fixedTimeSeconds) {
|
||||
return accumulateForceZones(zones, fixedTimeSeconds, true);
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::accumulateExternalForceZones(std::span<const VolumeFluidWindZone> zones,
|
||||
float fixedTimeSeconds) {
|
||||
return accumulateForceZones(zones, fixedTimeSeconds, false);
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::accumulateForceZones(std::span<const VolumeFluidWindZone> zones, float fixedTimeSeconds,
|
||||
bool asWind) {
|
||||
constexpr size_t kMaxZones = 64;
|
||||
constexpr size_t kCandidateBudget = 4000000;
|
||||
if (zones.size() > kMaxZones || !std::isfinite(fixedTimeSeconds) || fixedTimeSeconds < 0.f ||
|
||||
fixedTimeSeconds > 1e9f)
|
||||
return invalid("Invalid wind-zone count or fixed time");
|
||||
if (zones.empty() || impl_->state.empty()) return Result<void>::success();
|
||||
|
||||
size_t sphericalCount = 0;
|
||||
glm::vec3 ambient(0.f);
|
||||
std::vector<float> resolvedIntensity;
|
||||
resolvedIntensity.reserve(zones.size());
|
||||
for (const auto& zone : zones) {
|
||||
const bool validType =
|
||||
zone.type == VolumeFluidWindZoneType::Ambient || zone.type == VolumeFluidWindZoneType::Spherical;
|
||||
if (!validType || !finite(zone.center) || !finite(zone.direction) || glm::length(zone.direction) > 1000.f ||
|
||||
!range(zone.intensity, -1000.f, 1000.f) || !range(zone.turbulence, -1000.f, 1000.f) ||
|
||||
!range(zone.turbulenceFrequency, 0.f, 1000.f) || !range(zone.turbulenceSeed, -1000000.f, 1000000.f) ||
|
||||
(zone.type == VolumeFluidWindZoneType::Spherical && !range(zone.radius, .001f, 10000.f)))
|
||||
return invalid("Invalid wind-zone field");
|
||||
const float noise = std::clamp(
|
||||
.5f + .5f * glm::perlin(glm::vec2(fixedTimeSeconds * zone.turbulenceFrequency, zone.turbulenceSeed)), 0.f,
|
||||
1.f);
|
||||
const float intensity = zone.intensity + noise * zone.turbulence;
|
||||
resolvedIntensity.push_back(intensity);
|
||||
if (zone.type == VolumeFluidWindZoneType::Ambient)
|
||||
ambient += zone.direction * intensity;
|
||||
else
|
||||
++sphericalCount;
|
||||
}
|
||||
if (sphericalCount != 0 && impl_->state.size() > kCandidateBudget / sphericalCount)
|
||||
return invalid("Wind-zone candidate budget exceeded");
|
||||
|
||||
auto& current = asWind ? impl_->winds : impl_->externalForces;
|
||||
auto& candidate = asWind ? impl_->windScratch : impl_->externalForceScratch;
|
||||
if (current.empty())
|
||||
candidate.assign(impl_->state.size(), glm::vec3(0.f));
|
||||
else
|
||||
candidate.assign(current.begin(), current.end());
|
||||
for (size_t particleIndex = 0; particleIndex < impl_->state.size(); ++particleIndex) {
|
||||
auto force = candidate[particleIndex] + ambient;
|
||||
for (size_t zoneIndex = 0; zoneIndex < zones.size(); ++zoneIndex) {
|
||||
const auto& zone = zones[zoneIndex];
|
||||
if (zone.type != VolumeFluidWindZoneType::Spherical) continue;
|
||||
const auto distance = impl_->state[particleIndex].position - zone.center;
|
||||
const float squaredDistance = glm::dot(distance, distance);
|
||||
const float squaredRadius = zone.radius * zone.radius;
|
||||
if (squaredDistance >= squaredRadius) continue;
|
||||
const float falloff = std::clamp((squaredRadius - squaredDistance) / squaredRadius, 0.f, 1.f);
|
||||
if (zone.radial) {
|
||||
force += distance / (std::sqrt(squaredDistance) + std::numeric_limits<float>::epsilon()) * falloff *
|
||||
resolvedIntensity[zoneIndex];
|
||||
} else {
|
||||
force += zone.direction * falloff * resolvedIntensity[zoneIndex];
|
||||
}
|
||||
}
|
||||
if (!finite(force) || glm::length(force) > 1000.f)
|
||||
return invalid(asWind ? "Accumulated wind-zone velocity exceeds limit"
|
||||
: "Accumulated external force exceeds limit");
|
||||
candidate[particleIndex] = force;
|
||||
}
|
||||
current.swap(candidate);
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::stepWithThermalContacts(float seconds, unsigned substeps,
|
||||
std::span<const VolumeFluidThermalRule> rules) {
|
||||
EV_PROFILE_MODULE("fluids", "VolumeFluid::step");
|
||||
if (!std::isfinite(seconds) || seconds <= 0.f || seconds > 1.f / 30.f || substeps == 0 || substeps > 32 ||
|
||||
seconds / float(substeps) > 1.f / 120.f)
|
||||
return invalid("Invalid timestep or substep count");
|
||||
if (rules.size() > 1024) return invalid("Too many thermal rules");
|
||||
auto& ordered = impl_->thermalRulesScratch;
|
||||
ordered.assign(rules.begin(), rules.end());
|
||||
std::sort(ordered.begin(), ordered.end(),
|
||||
[](const auto& a, const auto& b) { return a.colliderLabel < b.colliderLabel; });
|
||||
for (size_t i = 0; i < ordered.size(); ++i) {
|
||||
const auto& rule = ordered[i];
|
||||
if (!range(rule.rate, -1000000.f, 1000000.f) || !range(rule.minimumViscosity, 0.f, 100.f) ||
|
||||
!range(rule.maximumViscosity, rule.minimumViscosity, 100.f) || !range(rule.minimumCohesion, 0.f, 10.f) ||
|
||||
!range(rule.maximumCohesion, rule.minimumCohesion, 10.f) ||
|
||||
(i > 0 && ordered[i - 1].colliderLabel == rule.colliderLabel) ||
|
||||
std::none_of(impl_->colliders.begin(), impl_->colliders.end(),
|
||||
[&](const auto& c) { return c.label == rule.colliderLabel; }))
|
||||
return invalid("Invalid thermal rule or collider label");
|
||||
}
|
||||
auto& contactParticles = impl_->thermalContactParticles;
|
||||
contactParticles.clear();
|
||||
struct Motion {
|
||||
size_t particle;
|
||||
glm::vec3 start, target, velocity;
|
||||
glm::quat startOrientation, targetOrientation;
|
||||
glm::vec3 localPosition;
|
||||
glm::quat localOrientation;
|
||||
unsigned colliderLabel;
|
||||
size_t colliderIndex;
|
||||
glm::vec3 inertialReaction;
|
||||
glm::vec3 angularVelocity, angularReaction;
|
||||
bool constrainOrientation;
|
||||
bool dynamic;
|
||||
float compliance, breakThreshold;
|
||||
glm::vec3 constraintReaction{0.f};
|
||||
glm::vec3 dynamicAngularReaction{0.f};
|
||||
bool broken = false;
|
||||
};
|
||||
std::vector<Motion> motions;
|
||||
if (!impl_->attachments.empty()) {
|
||||
std::vector<std::pair<unsigned, size_t>> labels;
|
||||
labels.reserve(impl_->colliders.size());
|
||||
for (size_t i = 0; i < impl_->colliders.size(); ++i) labels.emplace_back(impl_->colliders[i].label, i);
|
||||
std::sort(labels.begin(), labels.end());
|
||||
motions.reserve(impl_->attachments.size());
|
||||
for (const auto& attachment : impl_->attachments) {
|
||||
const auto found = std::lower_bound(labels.begin(), labels.end(),
|
||||
std::pair<unsigned, size_t>{attachment.colliderLabel, 0});
|
||||
if (found == labels.end() || found->first != attachment.colliderLabel)
|
||||
return invalid("Missing solid attachment collider");
|
||||
const auto target = impl_->colliders[found->second].center +
|
||||
impl_->colliderRotations[found->second] * attachment.localPosition;
|
||||
const auto start = impl_->state[attachment.particleIndex].position;
|
||||
const auto velocity = (target - start) / seconds;
|
||||
const auto& collider = impl_->colliders[found->second];
|
||||
const auto colliderQ =
|
||||
glm::quat(collider.rotation.w, collider.rotation.x, collider.rotation.y, collider.rotation.z);
|
||||
const auto localQ = glm::quat(attachment.localOrientation.w, attachment.localOrientation.x,
|
||||
attachment.localOrientation.y, attachment.localOrientation.z);
|
||||
const auto startQ = particleQuaternion(impl_->state[attachment.particleIndex]);
|
||||
const auto targetQ = glm::normalize(colliderQ * localQ);
|
||||
if (!finite(target) || !finite(velocity) || glm::length(velocity) > 100.f ||
|
||||
glm::any(glm::notEqual(impl_->constrain(target), target)))
|
||||
return invalid("Solid attachment motion exceeds bounds or speed");
|
||||
const auto& particle = impl_->state[attachment.particleIndex];
|
||||
const float mass = impl_->volume * particle.material.density;
|
||||
const auto unconstrainedVelocity =
|
||||
particle.velocity + impl_->settings.gravity * particle.material.buoyancy * seconds;
|
||||
const auto targetAngular = collider.angularVelocity;
|
||||
const auto radii = particle.radii;
|
||||
const glm::vec3 principal =
|
||||
mass * .2f *
|
||||
glm::vec3(radii.y * radii.y + radii.z * radii.z, radii.x * radii.x + radii.z * radii.z,
|
||||
radii.x * radii.x + radii.y * radii.y);
|
||||
const auto orientation = glm::mat3_cast(startQ);
|
||||
const auto angularReaction =
|
||||
-orientation * glm::mat3(principal.x, 0.f, 0.f, 0.f, principal.y, 0.f, 0.f, 0.f, principal.z) *
|
||||
glm::transpose(orientation) * (targetAngular - particle.angularVelocity);
|
||||
motions.push_back(
|
||||
{attachment.particleIndex, start, target, velocity, startQ, targetQ, attachment.localPosition, localQ,
|
||||
attachment.colliderLabel, found->second,
|
||||
attachment.dynamic ? glm::vec3(0.f) : -mass * (velocity - unconstrainedVelocity),
|
||||
attachment.constrainOrientation ? targetAngular : particle.angularVelocity,
|
||||
!attachment.dynamic && attachment.constrainOrientation ? angularReaction : glm::vec3(0.f),
|
||||
attachment.constrainOrientation, attachment.dynamic, attachment.compliance,
|
||||
attachment.breakThreshold});
|
||||
}
|
||||
}
|
||||
auto& colliderEnds = impl_->colliderPoseScratch;
|
||||
auto& sdfColliderEnds = impl_->sdfColliderPoseScratch;
|
||||
auto& heightFieldEnds = impl_->heightFieldPoseScratch;
|
||||
colliderEnds = impl_->colliders;
|
||||
sdfColliderEnds.resize(impl_->sdfColliders.size());
|
||||
for (size_t i = 0; i < impl_->sdfColliders.size(); ++i) {
|
||||
const auto& collider = impl_->sdfColliders[i];
|
||||
sdfColliderEnds[i] = {collider.label, collider.position, collider.rotation,
|
||||
collider.scale, collider.velocity, collider.angularVelocity};
|
||||
}
|
||||
heightFieldEnds.resize(impl_->heightFieldColliders.size());
|
||||
for (size_t i = 0; i < impl_->heightFieldColliders.size(); ++i) {
|
||||
const auto& collider = impl_->heightFieldColliders[i];
|
||||
heightFieldEnds[i] = {collider.label, collider.position, collider.rotation, 1.f,
|
||||
collider.velocity, collider.angularVelocity};
|
||||
}
|
||||
auto interpolatedRotation = [](glm::vec4 rotation, glm::vec3 angularVelocity, float seconds, float fraction) {
|
||||
const auto end = glm::normalize(glm::quat(rotation.w, rotation.x, rotation.y, rotation.z));
|
||||
const float speed = glm::length(angularVelocity);
|
||||
if (speed <= 1e-7f) return end;
|
||||
return glm::normalize(glm::angleAxis(-speed * seconds * (1.f - fraction), angularVelocity / speed) * end);
|
||||
};
|
||||
for (size_t i = 0; i < sdfColliderEnds.size(); ++i)
|
||||
if (impl_->sdfColliders[i].inverted) {
|
||||
const auto& pose = sdfColliderEnds[i];
|
||||
const auto& sdf = impl_->sdfColliders[i].sdf;
|
||||
const auto startRotation =
|
||||
glm::mat3_cast(interpolatedRotation(pose.rotation, pose.angularVelocity, seconds, 0.f));
|
||||
const auto startPosition = pose.position - pose.velocity * seconds;
|
||||
const auto minimum = sdf.origin;
|
||||
const auto maximum = minimum + glm::vec3(sdf.dims - glm::ivec3(1)) * sdf.cellSize;
|
||||
for (unsigned corner = 0; corner < 8; ++corner) {
|
||||
const glm::vec3 world{corner & 1 ? impl_->settings.maximum.x : impl_->settings.minimum.x,
|
||||
corner & 2 ? impl_->settings.maximum.y : impl_->settings.minimum.y,
|
||||
corner & 4 ? impl_->settings.maximum.z : impl_->settings.minimum.z};
|
||||
const auto local = glm::transpose(startRotation) * (world - startPosition) / pose.scale;
|
||||
if (glm::any(glm::lessThan(local, minimum)) || glm::any(glm::greaterThan(local, maximum)))
|
||||
return invalid("Moving inverted SDF domain must contain solver bounds for the complete step");
|
||||
}
|
||||
}
|
||||
const auto restoreColliderEnds = [&]() {
|
||||
impl_->colliders.swap(colliderEnds);
|
||||
for (size_t i = 0; i < sdfColliderEnds.size(); ++i) {
|
||||
impl_->sdfColliders[i].position = sdfColliderEnds[i].position;
|
||||
impl_->sdfColliders[i].rotation = sdfColliderEnds[i].rotation;
|
||||
}
|
||||
for (size_t i = 0; i < heightFieldEnds.size(); ++i) {
|
||||
impl_->heightFieldColliders[i].position = heightFieldEnds[i].position;
|
||||
impl_->heightFieldColliders[i].rotation = heightFieldEnds[i].rotation;
|
||||
}
|
||||
impl_->colliderRotations.resize(impl_->colliders.size());
|
||||
for (size_t i = 0; i < impl_->colliders.size(); ++i) {
|
||||
const auto& r = impl_->colliders[i].rotation;
|
||||
impl_->colliderRotations[i] = glm::mat3_cast(glm::normalize(glm::quat(r.w, r.x, r.y, r.z)));
|
||||
}
|
||||
impl_->sdfColliderRotations.resize(impl_->sdfColliders.size());
|
||||
for (size_t i = 0; i < impl_->sdfColliders.size(); ++i) {
|
||||
const auto& r = impl_->sdfColliders[i].rotation;
|
||||
impl_->sdfColliderRotations[i] = glm::mat3_cast(glm::normalize(glm::quat(r.w, r.x, r.y, r.z)));
|
||||
}
|
||||
impl_->heightFieldColliderRotations.resize(impl_->heightFieldColliders.size());
|
||||
for (size_t i = 0; i < impl_->heightFieldColliders.size(); ++i) {
|
||||
const auto& r = impl_->heightFieldColliders[i].rotation;
|
||||
impl_->heightFieldColliderRotations[i] = glm::mat3_cast(glm::normalize(glm::quat(r.w, r.x, r.y, r.z)));
|
||||
}
|
||||
};
|
||||
auto& original = impl_->rollbackState;
|
||||
original = impl_->state;
|
||||
impl_->renderPreviousScratch.resize(impl_->state.size());
|
||||
for (size_t i = 0; i < impl_->state.size(); ++i) impl_->renderPreviousScratch[i] = impl_->state[i].position;
|
||||
auto& originalContacts = impl_->rollbackContacts;
|
||||
originalContacts = impl_->contacts;
|
||||
auto& originalAttachments = impl_->rollbackAttachments;
|
||||
originalAttachments = impl_->attachments;
|
||||
impl_->attachmentLabels.clear();
|
||||
impl_->attachmentOrientationConstraints.clear();
|
||||
impl_->attachmentStressImpulse.clear();
|
||||
if (!motions.empty()) {
|
||||
impl_->attachmentLabels.assign(impl_->state.size(), -1);
|
||||
impl_->attachmentOrientationConstraints.assign(impl_->state.size(), uint8_t(0));
|
||||
impl_->attachmentStressImpulse.assign(impl_->state.size(), glm::vec3(0.f));
|
||||
for (const auto& motion : motions) {
|
||||
if (motion.dynamic) continue;
|
||||
impl_->attachmentLabels[motion.particle] = int(motion.colliderLabel);
|
||||
impl_->attachmentOrientationConstraints[motion.particle] = motion.constrainOrientation ? 1 : 0;
|
||||
}
|
||||
}
|
||||
impl_->contacts.clear();
|
||||
for (auto& p : impl_->state)
|
||||
if (p.material.phase == VolumeFluidPhase::Solid) {
|
||||
p.velocity = glm::vec3(0.f);
|
||||
p.angularVelocity = glm::vec3(0.f);
|
||||
}
|
||||
for (unsigned k = 0; k < substeps; ++k) {
|
||||
const float fraction = float(k + 1) / float(substeps);
|
||||
for (size_t i = 0; i < colliderEnds.size(); ++i) {
|
||||
const auto& end = colliderEnds[i];
|
||||
auto& sample = impl_->colliders[i];
|
||||
sample.center = end.center - end.velocity * (seconds * (1.f - fraction));
|
||||
const auto q = interpolatedRotation(end.rotation, end.angularVelocity, seconds, fraction);
|
||||
sample.rotation = {q.x, q.y, q.z, q.w};
|
||||
impl_->colliderRotations[i] = glm::mat3_cast(q);
|
||||
}
|
||||
for (size_t i = 0; i < sdfColliderEnds.size(); ++i) {
|
||||
const auto& end = sdfColliderEnds[i];
|
||||
auto& sample = impl_->sdfColliders[i];
|
||||
sample.position = end.position - end.velocity * (seconds * (1.f - fraction));
|
||||
const auto q = interpolatedRotation(end.rotation, end.angularVelocity, seconds, fraction);
|
||||
sample.rotation = {q.x, q.y, q.z, q.w};
|
||||
impl_->sdfColliderRotations[i] = glm::mat3_cast(q);
|
||||
}
|
||||
for (size_t i = 0; i < heightFieldEnds.size(); ++i) {
|
||||
const auto& end = heightFieldEnds[i];
|
||||
auto& sample = impl_->heightFieldColliders[i];
|
||||
sample.position = end.position - end.velocity * (seconds * (1.f - fraction));
|
||||
const auto q = interpolatedRotation(end.rotation, end.angularVelocity, seconds, fraction);
|
||||
sample.rotation = {q.x, q.y, q.z, q.w};
|
||||
impl_->heightFieldColliderRotations[i] = glm::mat3_cast(q);
|
||||
}
|
||||
for (const auto& motion : motions) {
|
||||
if (motion.dynamic) continue;
|
||||
auto& p = impl_->state[motion.particle];
|
||||
p.position = glm::mix(motion.start, motion.target, float(k + 1) / float(substeps));
|
||||
p.velocity = p.material.phase == VolumeFluidPhase::Solid ? motion.velocity : glm::vec3(0.f);
|
||||
if (motion.constrainOrientation) {
|
||||
p.angularVelocity = motion.angularVelocity;
|
||||
const auto orientation = glm::normalize(
|
||||
glm::slerp(motion.startOrientation, motion.targetOrientation, float(k + 1) / float(substeps)));
|
||||
p.orientation = {orientation.x, orientation.y, orientation.z, orientation.w};
|
||||
}
|
||||
}
|
||||
if (motions.empty()) {
|
||||
if (impl_->externalForces.empty())
|
||||
impl_->substep<false, false>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
|
||||
else
|
||||
impl_->substep<false, true>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
|
||||
} else if (impl_->externalForces.empty()) {
|
||||
impl_->substep<true, false>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
|
||||
} else {
|
||||
impl_->substep<true, true>(seconds / float(substeps), rules.empty() ? nullptr : &contactParticles);
|
||||
}
|
||||
if (!impl_->stitches.empty()) {
|
||||
const float stitchDt = seconds / float(substeps);
|
||||
impl_->stitchStart.resize(impl_->state.size());
|
||||
impl_->stitchDelta.resize(impl_->state.size());
|
||||
impl_->stitchCounts.resize(impl_->state.size());
|
||||
impl_->stitchTouched.clear();
|
||||
impl_->stitchTouched.reserve(impl_->stitches.size() * 2);
|
||||
for (const auto& stitch : impl_->stitches) {
|
||||
impl_->stitchTouched.push_back(stitch.particleIndex1);
|
||||
impl_->stitchTouched.push_back(stitch.particleIndex2);
|
||||
impl_->stitchStart[stitch.particleIndex1] = impl_->state[stitch.particleIndex1].position;
|
||||
impl_->stitchStart[stitch.particleIndex2] = impl_->state[stitch.particleIndex2].position;
|
||||
}
|
||||
std::sort(impl_->stitchTouched.begin(), impl_->stitchTouched.end());
|
||||
impl_->stitchTouched.erase(std::unique(impl_->stitchTouched.begin(), impl_->stitchTouched.end()),
|
||||
impl_->stitchTouched.end());
|
||||
impl_->stitchLambdas.assign(impl_->stitches.size(), 0.f);
|
||||
for (unsigned iteration = 0; iteration < impl_->settings.iterations; ++iteration) {
|
||||
for (const unsigned index : impl_->stitchTouched) {
|
||||
impl_->stitchDelta[index] = glm::vec3(0.f);
|
||||
impl_->stitchCounts[index] = 0;
|
||||
}
|
||||
for (size_t stitchIndex = 0; stitchIndex < impl_->stitches.size(); ++stitchIndex) {
|
||||
const auto& stitch = impl_->stitches[stitchIndex];
|
||||
const auto fixed = [&](unsigned index) {
|
||||
return impl_->state[index].material.phase == VolumeFluidPhase::Solid ||
|
||||
impl_->grabberLabels[index] != Impl::noGrabber ||
|
||||
(!impl_->attachmentLabels.empty() && impl_->attachmentLabels[index] >= 0);
|
||||
};
|
||||
const float w1 = fixed(stitch.particleIndex1)
|
||||
? 0.f
|
||||
: 1.f / (impl_->volume * impl_->state[stitch.particleIndex1].material.density);
|
||||
const float w2 = fixed(stitch.particleIndex2)
|
||||
? 0.f
|
||||
: 1.f / (impl_->volume * impl_->state[stitch.particleIndex2].material.density);
|
||||
const auto distance =
|
||||
impl_->state[stitch.particleIndex1].position - impl_->state[stitch.particleIndex2].position;
|
||||
const float length = glm::length(distance);
|
||||
const float alpha = stitch.compliance / (stitchDt * stitchDt);
|
||||
const float deltaLambda = (-length - alpha * impl_->stitchLambdas[stitchIndex]) /
|
||||
(w1 + w2 + alpha + std::numeric_limits<float>::epsilon());
|
||||
const auto delta = deltaLambda * distance / (length + std::numeric_limits<float>::epsilon());
|
||||
impl_->stitchDelta[stitch.particleIndex1] += delta * w1;
|
||||
impl_->stitchDelta[stitch.particleIndex2] -= delta * w2;
|
||||
++impl_->stitchCounts[stitch.particleIndex1];
|
||||
++impl_->stitchCounts[stitch.particleIndex2];
|
||||
impl_->stitchLambdas[stitchIndex] += deltaLambda;
|
||||
}
|
||||
for (const unsigned index : impl_->stitchTouched)
|
||||
if (impl_->stitchCounts[index] != 0)
|
||||
impl_->state[index].position += impl_->stitchDelta[index] / float(impl_->stitchCounts[index]);
|
||||
}
|
||||
for (const unsigned index : impl_->stitchTouched)
|
||||
if (impl_->stitchCounts[index] != 0)
|
||||
impl_->state[index].velocity +=
|
||||
(impl_->state[index].position - impl_->stitchStart[index]) / stitchDt;
|
||||
}
|
||||
const float substepSeconds = seconds / float(substeps);
|
||||
for (auto& motion : motions) {
|
||||
if (!motion.dynamic || motion.broken) continue;
|
||||
auto& particle = impl_->state[motion.particle];
|
||||
const auto& collider = impl_->colliders[motion.colliderIndex];
|
||||
const auto& rotation = impl_->colliderRotations[motion.colliderIndex];
|
||||
const auto target = collider.center + rotation * motion.localPosition;
|
||||
const float mass = impl_->volume * particle.material.density;
|
||||
const float inverseMass = 1.f / mass;
|
||||
const float alpha = motion.compliance / (substepSeconds * substepSeconds);
|
||||
const float response = inverseMass / (inverseMass + alpha);
|
||||
const auto correction = (target - particle.position) * response;
|
||||
particle.position += correction;
|
||||
particle.velocity += correction / substepSeconds;
|
||||
motion.constraintReaction -= mass * correction / substepSeconds;
|
||||
float force = mass * glm::length(correction) / (substepSeconds * substepSeconds);
|
||||
|
||||
if (motion.constrainOrientation) {
|
||||
const auto colliderQ =
|
||||
glm::quat(collider.rotation.w, collider.rotation.x, collider.rotation.y, collider.rotation.z);
|
||||
const auto currentQ = particleQuaternion(particle);
|
||||
const auto desiredQ = glm::normalize(colliderQ * motion.localOrientation);
|
||||
const auto nextQ = glm::normalize(glm::slerp(currentQ, desiredQ, response));
|
||||
auto deltaQ = glm::normalize(nextQ * glm::conjugate(currentQ));
|
||||
if (deltaQ.w < 0.f) deltaQ = -deltaQ;
|
||||
const float angle = 2.f * std::acos(std::clamp(deltaQ.w, -1.f, 1.f));
|
||||
glm::vec3 deltaAngular(0.f);
|
||||
const float sine = std::sqrt(std::max(0.f, 1.f - deltaQ.w * deltaQ.w));
|
||||
if (sine > 1e-6f)
|
||||
deltaAngular = glm::vec3(deltaQ.x, deltaQ.y, deltaQ.z) * (angle / sine / substepSeconds);
|
||||
particle.orientation = {nextQ.x, nextQ.y, nextQ.z, nextQ.w};
|
||||
particle.angularVelocity += deltaAngular;
|
||||
const auto radii = particle.radii;
|
||||
const float inertia =
|
||||
mass * .2f *
|
||||
((radii.y * radii.y + radii.z * radii.z) + (radii.x * radii.x + radii.z * radii.z) +
|
||||
(radii.x * radii.x + radii.y * radii.y)) /
|
||||
3.f;
|
||||
motion.dynamicAngularReaction -= deltaAngular * inertia;
|
||||
}
|
||||
if (force > motion.breakThreshold) motion.broken = true;
|
||||
}
|
||||
for (const auto& p : impl_->state)
|
||||
if (!finite(p.position) || !finite(p.velocity) || !finite(p.angularVelocity) ||
|
||||
glm::length(p.angularVelocity) > 1000.f || !finite(p.color) || !finite(p.data) ||
|
||||
(p.material.phase == VolumeFluidPhase::Solid &&
|
||||
glm::any(glm::notEqual(impl_->constrain(p.position), p.position)))) {
|
||||
impl_->state = original;
|
||||
impl_->contacts = originalContacts;
|
||||
impl_->attachments = originalAttachments;
|
||||
restoreColliderEnds();
|
||||
return Result<void>::failure(Diagnostic::error(
|
||||
DiagnosticCode::Failed, "Nonfinite state or solid outside bounds; step rolled back",
|
||||
"fluids.volume.step"));
|
||||
}
|
||||
}
|
||||
restoreColliderEnds();
|
||||
if (std::any_of(motions.begin(), motions.end(), [](const auto& motion) { return motion.broken; })) {
|
||||
std::erase_if(impl_->attachments, [&](const auto& attachment) {
|
||||
return std::any_of(motions.begin(), motions.end(), [&](const auto& motion) {
|
||||
return motion.broken && motion.particle == attachment.particleIndex &&
|
||||
motion.colliderLabel == attachment.colliderLabel;
|
||||
});
|
||||
});
|
||||
}
|
||||
// The final substep's grid still describes these unchanged positions.
|
||||
auto propagated = impl_->propagateSolidification();
|
||||
if (!propagated) {
|
||||
impl_->state = original;
|
||||
impl_->contacts = originalContacts;
|
||||
impl_->attachments = originalAttachments;
|
||||
return propagated;
|
||||
}
|
||||
impl_->attachmentReactionScratch.clear();
|
||||
impl_->attachmentReactionScratch.reserve(motions.size());
|
||||
for (const auto& motion : motions) {
|
||||
const auto impulse =
|
||||
motion.inertialReaction + motion.constraintReaction + impl_->attachmentStressImpulse[motion.particle];
|
||||
const auto angularImpulse = motion.angularReaction + motion.dynamicAngularReaction;
|
||||
if (glm::dot(impulse, impulse) > 0.f || glm::dot(angularImpulse, angularImpulse) > 0.f)
|
||||
impl_->attachmentReactionScratch.push_back({motion.colliderLabel, motion.target, impulse, angularImpulse});
|
||||
}
|
||||
impl_->attachmentReactions.swap(impl_->attachmentReactionScratch);
|
||||
auto& hits = impl_->thermalHitScratch;
|
||||
hits.clear();
|
||||
hits.reserve(contactParticles.size());
|
||||
for (size_t i = 0; i < contactParticles.size(); ++i)
|
||||
hits.push_back((uint64_t(impl_->contacts[i].colliderLabel) << 32u) | uint64_t(contactParticles[i]));
|
||||
std::sort(hits.begin(), hits.end());
|
||||
hits.erase(std::unique(hits.begin(), hits.end()), hits.end());
|
||||
for (const uint64_t hit : hits) {
|
||||
const unsigned label = unsigned(hit >> 32u);
|
||||
const auto rule = std::lower_bound(ordered.begin(), ordered.end(), label,
|
||||
[](const auto& item, unsigned value) { return item.colliderLabel < value; });
|
||||
if (rule == ordered.end() || rule->colliderLabel != label) continue;
|
||||
auto& data = impl_->state[size_t(hit & 0xFFFFFFFFull)].data;
|
||||
data.x = std::clamp(data.x + rule->rate * seconds, rule->minimumViscosity, rule->maximumViscosity);
|
||||
data.y = std::clamp(data.y + rule->rate * seconds, rule->minimumCohesion, rule->maximumCohesion);
|
||||
}
|
||||
// Reuse the neighbor-link scratch as the old-to-new index map after stepping.
|
||||
impl_->next.resize(impl_->state.size());
|
||||
impl_->grabberLabelScratch.resize(impl_->state.size());
|
||||
impl_->grabberLocalScratch.resize(impl_->state.size());
|
||||
size_t remaining = 0;
|
||||
for (size_t i = 0; i < impl_->state.size(); ++i) {
|
||||
auto& p = impl_->state[i];
|
||||
const bool expires = p.life > 0.f && (p.life -= seconds) <= 0.f;
|
||||
impl_->next[i] = expires ? -1 : int(remaining);
|
||||
if (expires) impl_->recordParticleEvent(VolumeFluidParticleEventType::Killed, unsigned(i), p);
|
||||
if (!expires) {
|
||||
if (remaining != i) impl_->state[remaining] = p;
|
||||
impl_->renderPreviousScratch[remaining] = impl_->renderPreviousScratch[i];
|
||||
impl_->grabberLabelScratch[remaining] = impl_->grabberLabels[i];
|
||||
impl_->grabberLocalScratch[remaining] = impl_->grabberLocalPositions[i];
|
||||
++remaining;
|
||||
}
|
||||
}
|
||||
impl_->state.resize(remaining);
|
||||
impl_->renderPreviousScratch.resize(remaining);
|
||||
impl_->renderPrevious.swap(impl_->renderPreviousScratch);
|
||||
impl_->grabberLabelScratch.resize(remaining);
|
||||
impl_->grabberLabels.swap(impl_->grabberLabelScratch);
|
||||
impl_->grabberLocalScratch.resize(remaining);
|
||||
impl_->grabberLocalPositions.swap(impl_->grabberLocalScratch);
|
||||
std::erase_if(impl_->attachments, [&](auto& a) {
|
||||
const int mapped = impl_->next[a.particleIndex];
|
||||
if (mapped < 0) return true;
|
||||
a.particleIndex = unsigned(mapped);
|
||||
return false;
|
||||
});
|
||||
std::erase_if(impl_->stitches, [&](auto& stitch) {
|
||||
const int first = impl_->next[stitch.particleIndex1];
|
||||
const int second = impl_->next[stitch.particleIndex2];
|
||||
if (first < 0 || second < 0) return true;
|
||||
stitch.particleIndex1 = unsigned(first);
|
||||
stitch.particleIndex2 = unsigned(second);
|
||||
return false;
|
||||
});
|
||||
std::erase_if(impl_->simplexes, [&](auto& simplex) {
|
||||
for (unsigned i = 0; i < simplex.size; ++i) {
|
||||
const int mapped = impl_->next[simplex.particleIndices[i]];
|
||||
if (mapped < 0) return true;
|
||||
simplex.particleIndices[i] = unsigned(mapped);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
impl_->winds.clear();
|
||||
impl_->externalForces.clear();
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
std::vector<VolumeFluidParticle> VolumeFluid::particles() const { return impl_->state; }
|
||||
std::span<const VolumeFluidParticle> VolumeFluid::particleView() const { return impl_->state; }
|
||||
Result<void> VolumeFluid::stepWithColliders(float seconds, unsigned substeps,
|
||||
std::span<const VolumeFluidCollider> colliders,
|
||||
std::span<const VolumeFluidThermalRule> rules) {
|
||||
auto previousColliders = impl_->colliders;
|
||||
auto previousRotations = impl_->colliderRotations;
|
||||
auto previousAttachments = impl_->attachments;
|
||||
auto configured = setColliders(colliders);
|
||||
if (!configured) return configured;
|
||||
auto advanced = stepWithThermalContacts(seconds, substeps, rules);
|
||||
if (!advanced) {
|
||||
impl_->colliders.swap(previousColliders);
|
||||
impl_->colliderRotations.swap(previousRotations);
|
||||
impl_->attachments.swap(previousAttachments);
|
||||
}
|
||||
return advanced;
|
||||
}
|
||||
size_t VolumeFluid::particleCount() const { return impl_->state.size(); }
|
||||
size_t VolumeFluid::availableCapacity() const { return impl_->settings.capacity - impl_->state.size(); }
|
||||
float VolumeFluid::spacing() const { return impl_->settings.spacing; }
|
||||
Result<void> VolumeFluid::setGravity(glm::vec3 gravity) {
|
||||
if (!finite(gravity) || glm::length(gravity) > 1000.f) return invalid("Invalid world-space gravity");
|
||||
impl_->settings.gravity = gravity;
|
||||
return Result<void>::success();
|
||||
}
|
||||
glm::vec3 VolumeFluid::gravity() const { return impl_->settings.gravity; }
|
||||
Result<void> VolumeFluid::copyInterpolatedPositions(float alpha, std::vector<glm::vec3>& positions) const {
|
||||
if (!std::isfinite(alpha) || alpha < 0.f || alpha > 1.f) return invalid("Invalid render interpolation alpha");
|
||||
positions.resize(impl_->state.size());
|
||||
for (size_t i = 0; i < impl_->state.size(); ++i) positions[i] = interpolatedPositionUnchecked(i, alpha);
|
||||
return Result<void>::success();
|
||||
}
|
||||
Result<float> VolumeFluid::copyInterpolatedRenderData(float alpha, std::vector<glm::vec3>& positions,
|
||||
std::vector<glm::vec4>& colors) const {
|
||||
if (!std::isfinite(alpha) || alpha < 0.f || alpha > 1.f)
|
||||
return Result<float>::failure(invalid("Invalid render interpolation alpha").status());
|
||||
positions.resize(impl_->state.size());
|
||||
colors.resize(impl_->state.size());
|
||||
for (size_t i = 0; i < impl_->state.size(); ++i) {
|
||||
positions[i] = interpolatedPositionUnchecked(i, alpha);
|
||||
colors[i] = impl_->state[i].color;
|
||||
}
|
||||
return Result<float>::success(impl_->settings.spacing);
|
||||
}
|
||||
glm::vec3 VolumeFluid::interpolatedPositionUnchecked(size_t particleIndex, float alpha) const {
|
||||
return glm::mix(impl_->renderPrevious[particleIndex], impl_->state[particleIndex].position, alpha);
|
||||
}
|
||||
float VolumeFluid::copyRenderData(std::vector<glm::vec3>& positions, std::vector<glm::vec4>& colors) const {
|
||||
positions.resize(impl_->state.size());
|
||||
colors.resize(impl_->state.size());
|
||||
for (size_t i = 0; i < impl_->state.size(); ++i) {
|
||||
positions[i] = impl_->state[i].position;
|
||||
colors[i] = impl_->state[i].color;
|
||||
}
|
||||
return impl_->settings.spacing;
|
||||
}
|
||||
float VolumeFluid::copySurfaceRenderData(std::vector<glm::vec3>& positions, std::vector<glm::vec4>& colors,
|
||||
std::vector<glm::vec3>& radii, std::vector<glm::vec4>& orientations) const {
|
||||
const size_t count = impl_->state.size();
|
||||
positions.resize(count);
|
||||
colors.resize(count);
|
||||
radii.resize(count);
|
||||
orientations.resize(count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const auto& particle = impl_->state[i];
|
||||
positions[i] = particle.position;
|
||||
colors[i] = particle.color;
|
||||
radii[i] = particle.radii;
|
||||
orientations[i] = particle.orientation;
|
||||
}
|
||||
return impl_->settings.spacing;
|
||||
}
|
||||
Result<float> VolumeFluid::copyInterpolatedSurfaceRenderData(float alpha, std::vector<glm::vec3>& positions,
|
||||
std::vector<glm::vec4>& colors,
|
||||
std::vector<glm::vec3>& radii,
|
||||
std::vector<glm::vec4>& orientations) const {
|
||||
if (!std::isfinite(alpha) || alpha < 0.f || alpha > 1.f)
|
||||
return Result<float>::failure(invalid("Invalid render interpolation alpha").status());
|
||||
const size_t count = impl_->state.size();
|
||||
positions.resize(count);
|
||||
colors.resize(count);
|
||||
radii.resize(count);
|
||||
orientations.resize(count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const auto& particle = impl_->state[i];
|
||||
positions[i] = interpolatedPositionUnchecked(i, alpha);
|
||||
colors[i] = particle.color;
|
||||
radii[i] = particle.radii;
|
||||
orientations[i] = particle.orientation;
|
||||
}
|
||||
return Result<float>::success(impl_->settings.spacing);
|
||||
}
|
||||
size_t VolumeFluid::copyPhaseRenderData(VolumeFluidPhase phase, std::vector<glm::vec3>& positions,
|
||||
std::vector<glm::vec4>& colors, size_t maxParticles) const {
|
||||
positions.clear();
|
||||
colors.clear();
|
||||
positions.reserve(std::min(maxParticles, impl_->state.size()));
|
||||
colors.reserve(std::min(maxParticles, impl_->state.size()));
|
||||
size_t selected = 0;
|
||||
for (const auto& particle : impl_->state) {
|
||||
if (particle.material.phase != phase) continue;
|
||||
if (selected < maxParticles) {
|
||||
positions.push_back(particle.position);
|
||||
colors.push_back(particle.color);
|
||||
}
|
||||
++selected;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
void VolumeFluid::clear() {
|
||||
impl_->state.clear();
|
||||
impl_->renderPrevious.clear();
|
||||
impl_->grabberLabels.clear();
|
||||
impl_->grabberLocalPositions.clear();
|
||||
impl_->contacts.clear();
|
||||
impl_->attachments.clear();
|
||||
impl_->stitches.clear();
|
||||
impl_->attachmentLabels.clear();
|
||||
impl_->attachmentOrientationConstraints.clear();
|
||||
impl_->simplexes.clear();
|
||||
impl_->attachmentReactions.clear();
|
||||
impl_->attachmentReactionScratch.clear();
|
||||
impl_->winds.clear();
|
||||
impl_->windScratch.clear();
|
||||
impl_->externalForces.clear();
|
||||
impl_->externalForceScratch.clear();
|
||||
impl_->particleEvents.clear();
|
||||
impl_->droppedParticleEvents = 0;
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::killParticle(unsigned particleIndex) {
|
||||
if (particleIndex >= impl_->state.size()) return invalid("Particle index is stale or out of range");
|
||||
impl_->recordParticleEvent(VolumeFluidParticleEventType::Killed, particleIndex, impl_->state[particleIndex]);
|
||||
impl_->state.erase(impl_->state.begin() + particleIndex);
|
||||
impl_->renderPrevious.erase(impl_->renderPrevious.begin() + particleIndex);
|
||||
impl_->grabberLabels.erase(impl_->grabberLabels.begin() + particleIndex);
|
||||
impl_->grabberLocalPositions.erase(impl_->grabberLocalPositions.begin() + particleIndex);
|
||||
std::erase_if(impl_->attachments, [&](auto& attachment) {
|
||||
if (attachment.particleIndex == particleIndex) return true;
|
||||
if (attachment.particleIndex > particleIndex) --attachment.particleIndex;
|
||||
return false;
|
||||
});
|
||||
std::erase_if(impl_->stitches, [&](auto& stitch) {
|
||||
if (stitch.particleIndex1 == particleIndex || stitch.particleIndex2 == particleIndex) return true;
|
||||
if (stitch.particleIndex1 > particleIndex) --stitch.particleIndex1;
|
||||
if (stitch.particleIndex2 > particleIndex) --stitch.particleIndex2;
|
||||
return false;
|
||||
});
|
||||
impl_->attachmentLabels.clear();
|
||||
impl_->attachmentOrientationConstraints.clear();
|
||||
std::erase_if(impl_->simplexes, [&](auto& simplex) {
|
||||
for (unsigned i = 0; i < simplex.size; ++i)
|
||||
if (simplex.particleIndices[i] == particleIndex) return true;
|
||||
for (unsigned i = 0; i < simplex.size; ++i)
|
||||
if (simplex.particleIndices[i] > particleIndex) --simplex.particleIndices[i];
|
||||
return false;
|
||||
});
|
||||
if (!impl_->winds.empty()) impl_->winds.erase(impl_->winds.begin() + particleIndex);
|
||||
if (!impl_->externalForces.empty()) impl_->externalForces.erase(impl_->externalForces.begin() + particleIndex);
|
||||
impl_->contacts.clear();
|
||||
impl_->attachmentReactions.clear();
|
||||
impl_->attachmentReactionScratch.clear();
|
||||
return Result<void>::success();
|
||||
}
|
||||
|
||||
Result<unsigned> VolumeFluid::killActorParticles(unsigned actorGroup) {
|
||||
if (actorGroup > 0x00ffffffu)
|
||||
return Result<unsigned>::failure(
|
||||
Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid actor group", "fluids.volume.killActor"));
|
||||
const size_t originalSize = impl_->state.size();
|
||||
const size_t removed = size_t(std::count_if(impl_->state.begin(), impl_->state.end(), [&](const auto& particle) {
|
||||
return particle.actorGroup == actorGroup;
|
||||
}));
|
||||
if (removed == 0)
|
||||
return Result<unsigned>::failure(Diagnostic::error(
|
||||
DiagnosticCode::NotFound, "Actor group has no live particles", "fluids.volume.killActor"));
|
||||
impl_->next.resize(originalSize);
|
||||
size_t remaining = 0;
|
||||
for (size_t i = 0; i < originalSize; ++i) {
|
||||
const bool erase = impl_->state[i].actorGroup == actorGroup;
|
||||
impl_->next[i] = erase ? -1 : int(remaining);
|
||||
if (erase) {
|
||||
impl_->recordParticleEvent(VolumeFluidParticleEventType::Killed, unsigned(i), impl_->state[i]);
|
||||
continue;
|
||||
}
|
||||
if (remaining != i) {
|
||||
impl_->state[remaining] = impl_->state[i];
|
||||
impl_->renderPrevious[remaining] = impl_->renderPrevious[i];
|
||||
impl_->grabberLabels[remaining] = impl_->grabberLabels[i];
|
||||
impl_->grabberLocalPositions[remaining] = impl_->grabberLocalPositions[i];
|
||||
if (!impl_->winds.empty()) impl_->winds[remaining] = impl_->winds[i];
|
||||
if (!impl_->externalForces.empty()) impl_->externalForces[remaining] = impl_->externalForces[i];
|
||||
}
|
||||
++remaining;
|
||||
}
|
||||
impl_->state.resize(remaining);
|
||||
impl_->renderPrevious.resize(remaining);
|
||||
impl_->grabberLabels.resize(remaining);
|
||||
impl_->grabberLocalPositions.resize(remaining);
|
||||
if (!impl_->winds.empty()) impl_->winds.resize(remaining);
|
||||
if (!impl_->externalForces.empty()) impl_->externalForces.resize(remaining);
|
||||
std::erase_if(impl_->attachments, [&](auto& attachment) {
|
||||
const int mapped = impl_->next[attachment.particleIndex];
|
||||
if (mapped < 0) return true;
|
||||
attachment.particleIndex = unsigned(mapped);
|
||||
return false;
|
||||
});
|
||||
std::erase_if(impl_->stitches, [&](auto& stitch) {
|
||||
const int first = impl_->next[stitch.particleIndex1], second = impl_->next[stitch.particleIndex2];
|
||||
if (first < 0 || second < 0) return true;
|
||||
stitch.particleIndex1 = unsigned(first);
|
||||
stitch.particleIndex2 = unsigned(second);
|
||||
return false;
|
||||
});
|
||||
std::erase_if(impl_->simplexes, [&](auto& simplex) {
|
||||
for (unsigned i = 0; i < simplex.size; ++i) {
|
||||
const int mapped = impl_->next[simplex.particleIndices[i]];
|
||||
if (mapped < 0) return true;
|
||||
simplex.particleIndices[i] = unsigned(mapped);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
impl_->attachmentLabels.clear();
|
||||
impl_->attachmentOrientationConstraints.clear();
|
||||
impl_->contacts.clear();
|
||||
impl_->attachmentReactions.clear();
|
||||
impl_->attachmentReactionScratch.clear();
|
||||
return Result<unsigned>::success(unsigned(removed));
|
||||
}
|
||||
|
||||
Result<unsigned> VolumeFluid::actorParticleCount(unsigned actorGroup) const {
|
||||
if (actorGroup > 0x00ffffffu)
|
||||
return Result<unsigned>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid actor group",
|
||||
"fluids.volume.actorParticleCount"));
|
||||
return Result<unsigned>::success(
|
||||
unsigned(std::count_if(impl_->state.begin(), impl_->state.end(),
|
||||
[&](const auto& particle) { return particle.actorGroup == actorGroup; })));
|
||||
}
|
||||
|
||||
Result<void> VolumeFluid::killActorParticle(unsigned actorGroup, unsigned actorParticleIndex) {
|
||||
if (actorGroup > 0x00ffffffu)
|
||||
return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Invalid actor group",
|
||||
"fluids.volume.killActorParticle"));
|
||||
unsigned localIndex = 0;
|
||||
for (size_t denseIndex = 0; denseIndex < impl_->state.size(); ++denseIndex) {
|
||||
if (impl_->state[denseIndex].actorGroup != actorGroup) continue;
|
||||
if (localIndex == actorParticleIndex) return killParticle(unsigned(denseIndex));
|
||||
++localIndex;
|
||||
}
|
||||
return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Actor particle index is stale or out of range",
|
||||
"fluids.volume.killActorParticle"));
|
||||
}
|
||||
|
||||
} // namespace eve::fluids
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <simplesquirrel/simplesquirrel.hpp>
|
||||
#include "common/Exception.h"
|
||||
#include "common/SquirrelBinding.h"
|
||||
#include "common/SquirrelOwnership.h"
|
||||
#include "fluids/Fluids.h"
|
||||
#include "fluids/VolumeFluidBindings.h"
|
||||
#include "fluids/VolumeFluidCodec.h"
|
||||
#include "fluids/VolumeFluidCoupling.h"
|
||||
#include "image/ImageData.h"
|
||||
#include "model3d/ModelData.h"
|
||||
#include "physics/Body3D.h"
|
||||
#include "physics/World3D.h"
|
||||
|
||||
namespace eve::fluids {
|
||||
namespace {
|
||||
template <class Emitter>
|
||||
void exposeEmitterState(ssq::Class& cls, HSQUIRRELVM vm) {
|
||||
cls.addFunc("isEmitting", [](Emitter* self) { return self->isEmitting(); });
|
||||
cls.addFunc("activeParticleCount", [vm](Emitter*, VolumeFluid* solver, ssq::Object emissionObject) {
|
||||
if (!solver)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing volume solver",
|
||||
"fluids.volume.actorParticleCount")),
|
||||
false, false);
|
||||
auto value = script::valueFromSquirrel(emissionObject);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto emission = decodeVolumeFluidEmission(value.value());
|
||||
if (!emission) return script::projectStatusResult(vm, emission.status(), false, false);
|
||||
return script::projectResult(vm, solver->actorParticleCount(emission.value().prototype.actorGroup),
|
||||
[](unsigned count) { return Value(int64_t(count)); });
|
||||
});
|
||||
cls.addFunc("killParticle", [vm](Emitter*, VolumeFluid* solver, ssq::Object emissionObject, int actorIndex) {
|
||||
if (!solver || actorIndex < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Missing solver or invalid actor particle index",
|
||||
"fluids.volume.killActorParticle")),
|
||||
false, false);
|
||||
auto value = script::valueFromSquirrel(emissionObject);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto emission = decodeVolumeFluidEmission(value.value());
|
||||
if (!emission) return script::projectStatusResult(vm, emission.status(), false, false);
|
||||
return script::projectResult(
|
||||
vm, solver->killActorParticle(emission.value().prototype.actorGroup, unsigned(actorIndex)));
|
||||
});
|
||||
cls.addFunc("snapshot", [vm](Emitter* self) {
|
||||
return script::projectStatusResult(vm, Status::success(), true, true,
|
||||
encodeVolumeFluidEmitterState(self->snapshot()));
|
||||
});
|
||||
cls.addFunc("restore", [vm](Emitter* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidEmitterState(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->restore(decoded.value()));
|
||||
});
|
||||
cls.addFunc("checkpoint", [vm](Emitter* self, VolumeFluid* solver, ssq::Object emissionObject) {
|
||||
if (!solver)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing volume solver",
|
||||
"fluids.volume.emitterCheckpoint")),
|
||||
false, false);
|
||||
auto value = script::valueFromSquirrel(emissionObject);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto emission = decodeVolumeFluidEmission(value.value());
|
||||
if (!emission) return script::projectStatusResult(vm, emission.status(), false, false);
|
||||
return script::projectStatusResult(
|
||||
vm, Status::success(), true, true,
|
||||
encodeVolumeFluidEmitterCheckpoint(captureVolumeFluidEmitterCheckpoint(*solver, emission.value(), *self)));
|
||||
});
|
||||
cls.addFunc("restoreCheckpoint", [vm](Emitter* self, VolumeFluid* solver, ssq::Object checkpointObject) {
|
||||
if (!solver)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing volume solver",
|
||||
"fluids.volume.emitterCheckpoint")),
|
||||
false, false);
|
||||
auto value = script::valueFromSquirrel(checkpointObject);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto checkpoint = decodeVolumeFluidEmitterCheckpoint(value.value());
|
||||
if (!checkpoint) return script::projectStatusResult(vm, checkpoint.status(), false, false);
|
||||
auto restored = restoreVolumeFluidEmitterCheckpoint(*solver, *self, checkpoint.value());
|
||||
if (!restored) return script::projectStatusResult(vm, restored.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true,
|
||||
encodeVolumeFluidEmission(checkpoint.value().emission));
|
||||
});
|
||||
}
|
||||
Result<VolumeFluidSnapshot> read(const ssq::Object& object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return Result<VolumeFluidSnapshot>::failure(value.status());
|
||||
return decodeVolumeFluid(value.value());
|
||||
}
|
||||
Result<std::vector<unsigned>> readParticleIndices(const ssq::Object& object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return Result<std::vector<unsigned>>::failure(value.status());
|
||||
if (!value.value().isArray() || value.value().arraySize() > 65536)
|
||||
return Result<std::vector<unsigned>>::failure(Diagnostic::error(
|
||||
DiagnosticCode::InvalidArgument, "Particle indices must be a bounded array", "fluids.volume.attachment"));
|
||||
std::vector<unsigned> indices;
|
||||
indices.reserve(value.value().arraySize());
|
||||
for (size_t i = 0; i < value.value().arraySize(); ++i) {
|
||||
const auto& item = value.value().at(i);
|
||||
if (!item.isInt64() || item.asInt() < 0 ||
|
||||
uint64_t(item.asInt()) > uint64_t(std::numeric_limits<unsigned>::max()))
|
||||
return Result<std::vector<unsigned>>::failure(
|
||||
Diagnostic::error(DiagnosticCode::InvalidArgument, "Particle indices must be nonnegative integers",
|
||||
"fluids.volume.attachment"));
|
||||
indices.push_back(unsigned(item.asInt()));
|
||||
}
|
||||
return Result<std::vector<unsigned>>::success(std::move(indices));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
} // namespace eve::fluids
|
||||
@@ -0,0 +1,816 @@
|
||||
#include "fluids/VolumeFluidBindingInternal.inc"
|
||||
|
||||
namespace eve::fluids {
|
||||
void exposeVolumeFluidType(ssq::Table& table) {
|
||||
const auto vm = table.getHandle();
|
||||
auto contactTracker = table.addClass<VolumeFluidContactTracker>(
|
||||
"VolumeFluidContactTracker",
|
||||
std::function<VolumeFluidContactTracker*()>([] { return new VolumeFluidContactTracker(); }), true);
|
||||
contactTracker.addFunc("advance", [vm](VolumeFluidContactTracker* self, VolumeFluid* fluid, float threshold) {
|
||||
if (!fluid)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing contact source",
|
||||
"fluids.volume.contacts")),
|
||||
false, false);
|
||||
auto events = self->advance(fluid->contacts(), threshold);
|
||||
if (!events) return script::projectStatusResult(vm, events.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true,
|
||||
encodeVolumeFluidContactEvents(events.value()));
|
||||
});
|
||||
contactTracker.addFunc("reset", [](VolumeFluidContactTracker* self) { self->reset(); });
|
||||
auto foam = table.addClass<VolumeFluidFoam>(
|
||||
"VolumeFluidFoam", std::function<VolumeFluidFoam*()>([] { return new VolumeFluidFoam(); }), true);
|
||||
foam.addFunc("snapshot", [vm](VolumeFluidFoam* self) {
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, encodeVolumeFluidFoam(self->snapshot()));
|
||||
});
|
||||
foam.addFunc("restore", [vm](VolumeFluidFoam* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidFoam(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->restore(decoded.value()));
|
||||
});
|
||||
foam.addFunc("advance", [vm](VolumeFluidFoam* self, VolumeFluid* fluid, VolumeFluidDiffuse* pool, float dt) {
|
||||
if (!fluid || !pool)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing foam source or pool",
|
||||
"fluids.volume.foam")),
|
||||
false, false);
|
||||
auto emitted = self->advance(*fluid, *pool, dt);
|
||||
if (!emitted) return script::projectStatusResult(vm, emitted.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(int64_t(emitted.value())));
|
||||
});
|
||||
foam.addFunc("advanceFromActor", [vm](VolumeFluidFoam* self, VolumeFluid* fluid, VolumeFluidDiffuse* pool, float dt,
|
||||
int actorGroup) {
|
||||
if (!fluid || !pool || actorGroup < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(
|
||||
DiagnosticCode::InvalidArgument,
|
||||
actorGroup < 0 ? "Actor group must be nonnegative" : "Missing foam source or pool",
|
||||
"fluids.volume.foam")),
|
||||
false, false);
|
||||
auto emitted = self->advanceFromActor(*fluid, *pool, dt, unsigned(actorGroup));
|
||||
if (!emitted) return script::projectStatusResult(vm, emitted.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(int64_t(emitted.value())));
|
||||
});
|
||||
foam.addFunc("advanceFromActorInterpolated", [vm](VolumeFluidFoam* self, VolumeFluid* fluid,
|
||||
VolumeFluidDiffuse* pool, float dt, int actorGroup, float alpha) {
|
||||
if (!fluid || !pool || actorGroup < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(
|
||||
DiagnosticCode::InvalidArgument,
|
||||
actorGroup < 0 ? "Actor group must be nonnegative" : "Missing foam source or pool",
|
||||
"fluids.volume.foam")),
|
||||
false, false);
|
||||
auto emitted = self->advanceFromActorInterpolated(*fluid, *pool, dt, unsigned(actorGroup), alpha);
|
||||
if (!emitted) return script::projectStatusResult(vm, emitted.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(int64_t(emitted.value())));
|
||||
});
|
||||
auto diffuse = table.addClass<VolumeFluidDiffuse>(
|
||||
"VolumeFluidDiffuse", std::function<VolumeFluidDiffuse*()>([] { return new VolumeFluidDiffuse(); }), true);
|
||||
diffuse.addFunc("snapshot", [vm](VolumeFluidDiffuse* self) {
|
||||
return script::projectStatusResult(vm, Status::success(), true, true,
|
||||
encodeVolumeFluidDiffuse(self->snapshot()));
|
||||
});
|
||||
diffuse.addFunc("restore", [vm](VolumeFluidDiffuse* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidDiffuse(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->restore(decoded.value()));
|
||||
});
|
||||
diffuse.addFunc("emit", [vm](VolumeFluidDiffuse* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidDiffuseParticles(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->emit(decoded.value()));
|
||||
});
|
||||
diffuse.addFunc("advance", [vm](VolumeFluidDiffuse* self, VolumeFluid* fluid, float dt, int minimumNeighbors) {
|
||||
if (!fluid)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing fluid field source",
|
||||
"fluids.volume.diffuse")),
|
||||
false, false);
|
||||
return script::projectResult(vm, self->advance(*fluid, dt, unsigned(minimumNeighbors)));
|
||||
});
|
||||
diffuse.addFunc("getParticleCount", [](VolumeFluidDiffuse* self) { return int(self->particleCount()); });
|
||||
diffuse.addFunc("getAvailableCapacity", [](VolumeFluidDiffuse* self) { return int(self->availableCapacity()); });
|
||||
diffuse.addFunc("clear", [](VolumeFluidDiffuse* self) { self->clear(); });
|
||||
auto coupling = table.addClass<VolumeFluidCoupling>(
|
||||
"VolumeFluidCoupling", std::function<VolumeFluidCoupling*()>([] { return new VolumeFluidCoupling(); }), true);
|
||||
coupling.addFunc("attach", [vm](VolumeFluidCoupling* self, physics::Body3D* body, ssq::Object object) {
|
||||
if (!body)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(
|
||||
Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing rigid body", "fluids.volume.coupling")),
|
||||
false, false);
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
Value::Array batch;
|
||||
batch.push_back(std::move(value).takeValue());
|
||||
auto decoded = decodeVolumeFluidColliders(Value(std::move(batch)));
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->attach(*body, decoded.value()[0]));
|
||||
});
|
||||
coupling.addFunc("detach", [](VolumeFluidCoupling* self, int label) { self->detach(unsigned(label)); });
|
||||
coupling.addFunc("setImpulseLimits", [vm](VolumeFluidCoupling* self, float linear, float angular) {
|
||||
return script::projectResult(vm, self->setImpulseLimits(linear, angular));
|
||||
});
|
||||
coupling.addFunc("lastClampedBodyCount", &VolumeFluidCoupling::lastClampedBodyCount);
|
||||
coupling.addFunc(
|
||||
"step", [vm](VolumeFluidCoupling* self, physics::World3D* world, VolumeFluid* fluid, float dt, int substeps) {
|
||||
if (!world || !fluid)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing rigid world or fluid",
|
||||
"fluids.volume.coupling")),
|
||||
false, false);
|
||||
auto result = self->step(*world, *fluid, dt, unsigned(substeps));
|
||||
if (!result) return script::projectStatusResult(vm, result.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(int64_t(result.value())));
|
||||
});
|
||||
coupling.addFunc("stepWithThermalContacts", [vm](VolumeFluidCoupling* self, physics::World3D* world,
|
||||
VolumeFluid* fluid, float dt, int substeps, ssq::Object object) {
|
||||
const auto failure = [&](const Status& status) {
|
||||
return script::projectStatusResult(vm, status, false, false);
|
||||
};
|
||||
if (!world || !fluid)
|
||||
return failure(Status::failure(Diagnostic::error(
|
||||
DiagnosticCode::InvalidArgument, "Missing rigid world or fluid", "fluids.volume.coupling")));
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return failure(value.status());
|
||||
auto rules = decodeVolumeFluidThermalRules(value.value());
|
||||
if (!rules) return failure(rules.status());
|
||||
auto result = self->step(*world, *fluid, dt, unsigned(substeps), rules.value());
|
||||
if (!result) return failure(result.status());
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(int64_t(result.value())));
|
||||
});
|
||||
auto jet = table.addClass<VolumeFluidJetEmitter>(
|
||||
"VolumeFluidJetEmitter", std::function<VolumeFluidJetEmitter*()>([] { return new VolumeFluidJetEmitter(); }),
|
||||
true);
|
||||
exposeEmitterState<VolumeFluidJetEmitter>(jet, vm);
|
||||
jet.addFunc(
|
||||
"advanceMoving", [vm](VolumeFluidJetEmitter* self, VolumeFluid* solver, ssq::Object object, ssq::Object begin,
|
||||
ssq::Object end, float dt, int limit, float threshold, float inheritVelocity) {
|
||||
const auto failure = [&](const Status& status) {
|
||||
return script::projectStatusResult(vm, status, false, false);
|
||||
};
|
||||
if (!solver)
|
||||
return failure(Status::failure(
|
||||
Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing volume solver", "fluids.volume.jet")));
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return failure(value.status());
|
||||
auto first = script::valueFromSquirrel(begin);
|
||||
if (!first) return failure(first.status());
|
||||
auto last = script::valueFromSquirrel(end);
|
||||
if (!last) return failure(last.status());
|
||||
auto emission = decodeVolumeFluidEmission(value.value());
|
||||
if (!emission) return failure(emission.status());
|
||||
auto firstPose = decodeVolumeFluidNozzlePose(first.value());
|
||||
if (!firstPose) return failure(firstPose.status());
|
||||
auto lastPose = decodeVolumeFluidNozzlePose(last.value());
|
||||
if (!lastPose) return failure(lastPose.status());
|
||||
auto emitted = self->advanceMoving(*solver, emission.value(), firstPose.value(), lastPose.value(), dt,
|
||||
unsigned(limit), threshold, inheritVelocity);
|
||||
if (!emitted) return failure(emitted.status());
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(int64_t(emitted.value())));
|
||||
});
|
||||
jet.addFunc("advance", [vm](VolumeFluidJetEmitter* self, VolumeFluid* solver, ssq::Object object, float dt,
|
||||
int limit, float threshold) {
|
||||
if (!solver)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(
|
||||
Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing volume solver", "fluids.volume.jet")),
|
||||
false, false);
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidEmission(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
auto emitted = self->advance(*solver, decoded.value(), dt, unsigned(limit), threshold);
|
||||
if (!emitted) return script::projectStatusResult(vm, emitted.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(int64_t(emitted.value())));
|
||||
});
|
||||
auto emitter = table.addClass<VolumeFluidEmitter>(
|
||||
"VolumeFluidEmitter", std::function<VolumeFluidEmitter*()>([] { return new VolumeFluidEmitter(); }), true);
|
||||
exposeEmitterState<VolumeFluidEmitter>(emitter, vm);
|
||||
emitter.addFunc("emitParticle",
|
||||
[vm](VolumeFluidEmitter* self, VolumeFluid* solver, ssq::Object object, float offset, float dt) {
|
||||
if (!solver)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Missing volume solver", "fluids.volume.emitter")),
|
||||
false, false);
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidEmission(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->emitParticle(*solver, decoded.value(), offset, dt),
|
||||
[](unsigned count) { return Value(int64_t(count)); });
|
||||
});
|
||||
emitter.addFunc("advance", [vm](VolumeFluidEmitter* self, VolumeFluid* solver, ssq::Object object, float dt,
|
||||
float rate, int limit, float threshold) {
|
||||
if (!solver)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Missing volume solver",
|
||||
"fluids.volume.emitter")),
|
||||
false, false);
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidEmission(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
auto emitted = self->advance(*solver, decoded.value(), dt, rate, unsigned(limit), threshold);
|
||||
if (!emitted) return script::projectStatusResult(vm, emitted.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(int64_t(emitted.value())));
|
||||
});
|
||||
emitter.addFunc("advanceBurst", [vm](VolumeFluidEmitter* self, VolumeFluid* solver, ssq::Object object, int count,
|
||||
float threshold) {
|
||||
if (!solver || count <= 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
solver ? "Burst count must be positive" : "Missing volume solver",
|
||||
"fluids.volume.emitter")),
|
||||
false, false);
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidEmission(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
auto emitted = self->advanceBurst(*solver, decoded.value(), unsigned(count), threshold);
|
||||
if (!emitted) return script::projectStatusResult(vm, emitted.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(int64_t(emitted.value())));
|
||||
});
|
||||
auto cls = table.addClass<VolumeFluid>(
|
||||
"VolumeFluid", std::function<VolumeFluid*()>([]() -> VolumeFluid* { return nullptr; }), true);
|
||||
cls.addFunc("step", [vm](VolumeFluid* self, float dt, int substeps) {
|
||||
return script::projectResult(vm, self->step(dt, unsigned(substeps)));
|
||||
});
|
||||
cls.addFunc("setGravity", [vm](VolumeFluid* self, float x, float y, float z) {
|
||||
return script::projectResult(vm, self->setGravity({x, y, z}));
|
||||
});
|
||||
cls.addFunc("setParticleWinds", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto winds = decodeVolumeFluidFieldPositions(value.value());
|
||||
if (!winds) return script::projectStatusResult(vm, winds.status(), false, false);
|
||||
return script::projectResult(vm, self->setParticleWinds(winds.value()));
|
||||
});
|
||||
cls.addFunc("setParticleExternalForces", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto forces = decodeVolumeFluidFieldPositions(value.value());
|
||||
if (!forces) return script::projectStatusResult(vm, forces.status(), false, false);
|
||||
return script::projectResult(vm, self->setParticleExternalForces(forces.value()));
|
||||
});
|
||||
cls.addFunc("accumulateWindZones", [vm](VolumeFluid* self, ssq::Object object, float fixedTimeSeconds) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto zones = decodeVolumeFluidWindZones(value.value());
|
||||
if (!zones) return script::projectStatusResult(vm, zones.status(), false, false);
|
||||
return script::projectResult(vm, self->accumulateWindZones(zones.value(), fixedTimeSeconds));
|
||||
});
|
||||
cls.addFunc("accumulateExternalForceZones", [vm](VolumeFluid* self, ssq::Object object, float fixedTimeSeconds) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto zones = decodeVolumeFluidWindZones(value.value());
|
||||
if (!zones) return script::projectStatusResult(vm, zones.status(), false, false);
|
||||
return script::projectResult(vm, self->accumulateExternalForceZones(zones.value(), fixedTimeSeconds));
|
||||
});
|
||||
cls.addFunc("stepWithThermalContacts", [vm](VolumeFluid* self, float dt, int substeps, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto rules = decodeVolumeFluidThermalRules(value.value());
|
||||
if (!rules) return script::projectStatusResult(vm, rules.status(), false, false);
|
||||
return script::projectResult(vm, self->stepWithThermalContacts(dt, unsigned(substeps), rules.value()));
|
||||
});
|
||||
cls.addFunc("snapshot", [vm](VolumeFluid* self) {
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, encodeVolumeFluid(self->snapshot()));
|
||||
});
|
||||
cls.addFunc("restore", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto decoded = read(object);
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->restore(decoded.value()));
|
||||
});
|
||||
cls.addFunc("emit", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidParticles(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->emit(decoded.value()));
|
||||
});
|
||||
cls.addFunc("configureParticleEvents", [vm](VolumeFluid* self, int capacity) {
|
||||
if (capacity < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Particle event capacity must be nonnegative",
|
||||
"fluids.volume.particleEvents")),
|
||||
false, false);
|
||||
return script::projectResult(vm, self->configureParticleEvents(unsigned(capacity)));
|
||||
});
|
||||
cls.addFunc("drainParticleEvents", [vm](VolumeFluid* self) {
|
||||
return script::projectStatusResult(vm, Status::success(), true, true,
|
||||
encodeVolumeFluidParticleEvents(self->drainParticleEvents()));
|
||||
});
|
||||
cls.addFunc("clear", [](VolumeFluid* self) { self->clear(); });
|
||||
cls.addFunc("killParticle", [vm](VolumeFluid* self, int particleIndex) {
|
||||
if (particleIndex < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Particle index must be nonnegative",
|
||||
"fluids.volume.killParticle")),
|
||||
false, false);
|
||||
return script::projectResult(vm, self->killParticle(unsigned(particleIndex)));
|
||||
});
|
||||
cls.addFunc("bindStaticParticles",
|
||||
[vm](VolumeFluid* self, ssq::Object object, int colliderLabel, bool constrainOrientation) {
|
||||
if (colliderLabel < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Attachment collider label must be nonnegative",
|
||||
"fluids.volume.attachment")),
|
||||
false, false);
|
||||
auto indices = readParticleIndices(object);
|
||||
if (!indices) return script::projectStatusResult(vm, indices.status(), false, false);
|
||||
return script::projectResult(
|
||||
vm, self->bindStaticParticles(indices.value(), unsigned(colliderLabel), constrainOrientation),
|
||||
[](unsigned count) { return Value(int64_t(count)); });
|
||||
});
|
||||
cls.addFunc("bindDynamicParticles", [vm](VolumeFluid* self, ssq::Object object, int colliderLabel, float compliance,
|
||||
float breakThreshold, bool constrainOrientation) {
|
||||
if (colliderLabel < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Attachment collider label must be nonnegative",
|
||||
"fluids.volume.attachment")),
|
||||
false, false);
|
||||
auto indices = readParticleIndices(object);
|
||||
if (!indices) return script::projectStatusResult(vm, indices.status(), false, false);
|
||||
return script::projectResult(vm,
|
||||
self->bindDynamicParticles(indices.value(), unsigned(colliderLabel), compliance,
|
||||
breakThreshold, constrainOrientation),
|
||||
[](unsigned count) { return Value(int64_t(count)); });
|
||||
});
|
||||
cls.addFunc("unbindStaticParticles", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto indices = readParticleIndices(object);
|
||||
if (!indices) return script::projectStatusResult(vm, indices.status(), false, false);
|
||||
return script::projectResult(vm, self->unbindStaticParticles(indices.value()),
|
||||
[](unsigned count) { return Value(int64_t(count)); });
|
||||
});
|
||||
cls.addFunc("setStitches", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto stitches = decodeVolumeFluidStitches(value.value());
|
||||
if (!stitches) return script::projectStatusResult(vm, stitches.status(), false, false);
|
||||
return script::projectResult(vm, self->setStitches(stitches.value()));
|
||||
});
|
||||
cls.addFunc("sampleField", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto positions = decodeVolumeFluidFieldPositions(value.value());
|
||||
if (!positions) return script::projectStatusResult(vm, positions.status(), false, false);
|
||||
auto samples = self->sampleField(positions.value());
|
||||
if (!samples) return script::projectStatusResult(vm, samples.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true,
|
||||
encodeVolumeFluidFieldSamples(samples.value()));
|
||||
});
|
||||
cls.addFunc("debugParticleGrid", [vm](VolumeFluid* self, int maxCells) {
|
||||
if (maxCells < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Particle-grid cell budget must be nonnegative",
|
||||
"fluids.volume.gridDebug")),
|
||||
false, false);
|
||||
auto cells = self->debugParticleGrid(unsigned(maxCells));
|
||||
if (!cells) return script::projectStatusResult(vm, cells.status(), false, false);
|
||||
Value::Array encoded;
|
||||
encoded.reserve(cells.value().size());
|
||||
for (const auto& cell : cells.value())
|
||||
encoded.push_back(Value::object({{"center", Value::array({cell.center.x, cell.center.y, cell.center.z})},
|
||||
{"size", Value::array({cell.size.x, cell.size.y, cell.size.z})},
|
||||
{"particleCount", int64_t(cell.particleCount)}}));
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(std::move(encoded)));
|
||||
});
|
||||
cls.addFunc("debugParticleFrames", [vm](VolumeFluid* self, int actorGroup, float size, int maxParticles) {
|
||||
if (actorGroup < 0 || maxParticles < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Particle-frame arguments must be nonnegative",
|
||||
"fluids.volume.frameDebug")),
|
||||
false, false);
|
||||
auto frames = self->debugParticleFrames(unsigned(actorGroup), size, unsigned(maxParticles));
|
||||
if (!frames) return script::projectStatusResult(vm, frames.status(), false, false);
|
||||
Value::Array encoded;
|
||||
encoded.reserve(frames.value().size());
|
||||
for (const auto& frame : frames.value())
|
||||
encoded.push_back(Value::object({{"particleIndex", int64_t(frame.particleIndex)},
|
||||
{"origin", Value::array({frame.origin.x, frame.origin.y, frame.origin.z})},
|
||||
{"x", Value::array({frame.x.x, frame.x.y, frame.x.z})},
|
||||
{"y", Value::array({frame.y.x, frame.y.y, frame.y.z})},
|
||||
{"z", Value::array({frame.z.x, frame.z.y, frame.z.z})}}));
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(std::move(encoded)));
|
||||
});
|
||||
cls.addFunc("particleInstances", [vm](VolumeFluid* self, int actorGroup, float sx, float sy, float sz, float alpha,
|
||||
int maxInstances) {
|
||||
if (actorGroup < 0 || maxInstances < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Particle-instance group and budget must be nonnegative",
|
||||
"fluids.volume.particleInstances")),
|
||||
false, false);
|
||||
auto copied = self->particleInstances(unsigned(actorGroup), {sx, sy, sz}, alpha, unsigned(maxInstances));
|
||||
if (!copied) return script::projectStatusResult(vm, copied.status(), false, false);
|
||||
Value::Array encoded;
|
||||
encoded.reserve(copied.value().size());
|
||||
for (const auto& instance : copied.value())
|
||||
encoded.push_back(Value::object(
|
||||
{{"particleIndex", int64_t(instance.particleIndex)},
|
||||
{"position", Value::array({instance.position.x, instance.position.y, instance.position.z})},
|
||||
{"orientation", Value::array({instance.orientation.x, instance.orientation.y, instance.orientation.z,
|
||||
instance.orientation.w})},
|
||||
{"scale", Value::array({instance.scale.x, instance.scale.y, instance.scale.z})},
|
||||
{"color", Value::array({instance.color.x, instance.color.y, instance.color.z, instance.color.w})}}));
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(std::move(encoded)));
|
||||
});
|
||||
cls.addFunc("particleImpostors", [vm](VolumeFluid* self, int actorGroup, float radiusScale, float r, float g,
|
||||
float b, float a, float alpha, int maxInstances) {
|
||||
if (actorGroup < 0 || maxInstances < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Particle-impostor group and budget must be nonnegative",
|
||||
"fluids.volume.particleImpostors")),
|
||||
false, false);
|
||||
auto copied =
|
||||
self->particleImpostors(unsigned(actorGroup), radiusScale, {r, g, b, a}, alpha, unsigned(maxInstances));
|
||||
if (!copied) return script::projectStatusResult(vm, copied.status(), false, false);
|
||||
Value::Array encoded;
|
||||
encoded.reserve(copied.value().size());
|
||||
for (const auto& instance : copied.value())
|
||||
encoded.push_back(Value::object(
|
||||
{{"particleIndex", int64_t(instance.particleIndex)},
|
||||
{"position", Value::array({instance.position.x, instance.position.y, instance.position.z})},
|
||||
{"orientation", Value::array({instance.orientation.x, instance.orientation.y, instance.orientation.z,
|
||||
instance.orientation.w})},
|
||||
{"scale", Value::array({instance.scale.x, instance.scale.y, instance.scale.z})},
|
||||
{"color", Value::array({instance.color.x, instance.color.y, instance.color.z, instance.color.w})}}));
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(std::move(encoded)));
|
||||
});
|
||||
cls.addFunc("debugSdfSlice",
|
||||
[vm](VolumeFluid* self, int colliderLabel, int axis, float slice, float maxDistance, int maxSamples) {
|
||||
if (colliderLabel < 0 || maxSamples < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"SDF slice label and budget must be nonnegative",
|
||||
"fluids.volume.sdfSliceDebug")),
|
||||
false, false);
|
||||
auto sampled = self->debugSdfSlice(unsigned(colliderLabel), VolumeFluidSdfSliceAxis(axis), slice,
|
||||
maxDistance, unsigned(maxSamples));
|
||||
if (!sampled) return script::projectStatusResult(vm, sampled.status(), false, false);
|
||||
const auto& result = sampled.value();
|
||||
Value::Array values;
|
||||
values.reserve(result.values.size());
|
||||
for (float value : result.values) values.emplace_back(value);
|
||||
return script::projectStatusResult(
|
||||
vm, Status::success(), true, true,
|
||||
Value::object({{"width", int64_t(result.width)},
|
||||
{"height", int64_t(result.height)},
|
||||
{"origin", Value::array({result.origin.x, result.origin.y, result.origin.z})},
|
||||
{"stepX", Value::array({result.stepX.x, result.stepX.y, result.stepX.z})},
|
||||
{"stepY", Value::array({result.stepY.x, result.stepY.y, result.stepY.z})},
|
||||
{"values", Value(std::move(values))}}));
|
||||
});
|
||||
cls.addFunc("applySolidColor", [vm](VolumeFluid* self, float r, float g, float b, float a) {
|
||||
return script::projectResult(vm, self->applySolidColor({r, g, b, a}));
|
||||
});
|
||||
cls.addFunc("applyVelocityColors", [vm](VolumeFluid* self, float sensibility) {
|
||||
return script::projectResult(vm, self->applyVelocityColors(sensibility));
|
||||
});
|
||||
cls.addFunc("applyActorGroupColors",
|
||||
[vm](VolumeFluid* self) { return script::projectResult(vm, self->applyActorGroupColors()); });
|
||||
cls.addFunc("applyDataColors", [vm](VolumeFluid* self, int channel, ssq::Object object) {
|
||||
if (channel < 0)
|
||||
return script::projectResult(vm, Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Invalid data color channel",
|
||||
"fluids.volume.applyDataColors")));
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto gradient = decodeVolumeFluidColorGradient(value.value());
|
||||
if (!gradient) return script::projectStatusResult(vm, gradient.status(), false, false);
|
||||
return script::projectResult(vm, self->applyDataColors(unsigned(channel), gradient.value()));
|
||||
});
|
||||
cls.addFunc("applyRandomColors", [vm](VolumeFluid* self, ssq::Object object, int seed) {
|
||||
if (seed < 0)
|
||||
return script::projectResult(vm, Result<void>::failure(Diagnostic::error(
|
||||
DiagnosticCode::InvalidArgument, "Invalid random color seed",
|
||||
"fluids.volume.applyRandomColors")));
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto gradient = decodeVolumeFluidColorGradient(value.value());
|
||||
if (!gradient) return script::projectStatusResult(vm, gradient.status(), false, false);
|
||||
return script::projectResult(vm, self->applyRandomColors(gradient.value(), uint32_t(seed)));
|
||||
});
|
||||
cls.addFunc("addRandomVelocity", [vm](VolumeFluid* self, float intensity, int seed) {
|
||||
if (seed < 0)
|
||||
return script::projectResult(vm, Result<void>::failure(Diagnostic::error(
|
||||
DiagnosticCode::InvalidArgument, "Invalid random velocity seed",
|
||||
"fluids.volume.addRandomVelocity")));
|
||||
return script::projectResult(vm, self->addRandomVelocity(intensity, uint32_t(seed)));
|
||||
});
|
||||
cls.addFunc("teleportActor", [vm](VolumeFluid* self, ssq::Object currentObject, ssq::Object targetObject) {
|
||||
auto currentValue = script::valueFromSquirrel(currentObject);
|
||||
if (!currentValue) return script::projectStatusResult(vm, currentValue.status(), false, false);
|
||||
auto targetValue = script::valueFromSquirrel(targetObject);
|
||||
if (!targetValue) return script::projectStatusResult(vm, targetValue.status(), false, false);
|
||||
auto current = decodeVolumeFluidNozzlePose(currentValue.value());
|
||||
if (!current) return script::projectStatusResult(vm, current.status(), false, false);
|
||||
auto target = decodeVolumeFluidNozzlePose(targetValue.value());
|
||||
if (!target) return script::projectStatusResult(vm, target.status(), false, false);
|
||||
return script::projectResult(vm, self->teleportActor(current.value().position, current.value().rotation,
|
||||
target.value().position, target.value().rotation));
|
||||
});
|
||||
cls.addFunc("actorMassProperties", [vm](VolumeFluid* self, int actorGroup) {
|
||||
if (actorGroup < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Actor group must be nonnegative",
|
||||
"fluids.volume.actorMass")),
|
||||
false, false);
|
||||
auto properties = self->actorMassProperties(unsigned(actorGroup));
|
||||
if (!properties) return script::projectStatusResult(vm, properties.status(), false, false);
|
||||
const auto& p = properties.value();
|
||||
return script::projectStatusResult(
|
||||
vm, Status::success(), true, true,
|
||||
Value::object({{"mass", p.mass},
|
||||
{"centerOfMass", Value::array({p.centerOfMass.x, p.centerOfMass.y, p.centerOfMass.z})},
|
||||
{"particleCount", int64_t(p.particleCount)}}));
|
||||
});
|
||||
cls.addFunc("setActorFilterCategory", [vm](VolumeFluid* self, int actorGroup, int category) {
|
||||
if (actorGroup < 0 || category < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Actor group and category must be nonnegative",
|
||||
"fluids.volume.filter")),
|
||||
false, false);
|
||||
return script::projectResult(vm, self->setActorFilterCategory(unsigned(actorGroup), unsigned(category)));
|
||||
});
|
||||
cls.addFunc("setActorCollisionFilter", [vm](VolumeFluid* self, int actorGroup, int64_t collisionFilter) {
|
||||
if (actorGroup < 0 || collisionFilter < 0 || collisionFilter > int64_t(0xffffffffu))
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
|
||||
"Actor group and packed collision filter are out of range",
|
||||
"fluids.volume.filter")),
|
||||
false, false);
|
||||
return script::projectResult(vm,
|
||||
self->setActorCollisionFilter(unsigned(actorGroup), unsigned(collisionFilter)));
|
||||
});
|
||||
cls.addFunc("updateActorMaterial", [vm](VolumeFluid* self, int actorGroup, ssq::Object emissionObject) {
|
||||
if (actorGroup < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Actor group must be nonnegative",
|
||||
"fluids.volume.actorMaterial")),
|
||||
false, false);
|
||||
auto value = script::valueFromSquirrel(emissionObject);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto emission = decodeVolumeFluidEmission(value.value());
|
||||
if (!emission) return script::projectStatusResult(vm, emission.status(), false, false);
|
||||
return script::projectResult(vm, self->updateActorMaterial(unsigned(actorGroup), emission.value().prototype));
|
||||
});
|
||||
cls.addFunc("setActorSelfCollisions", [vm](VolumeFluid* self, int actorGroup, bool enabled) {
|
||||
if (actorGroup < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Actor group must be nonnegative",
|
||||
"fluids.volume.selfCollisions")),
|
||||
false, false);
|
||||
return script::projectResult(vm, self->setActorSelfCollisions(unsigned(actorGroup), enabled));
|
||||
});
|
||||
cls.addFunc("killActorParticles", [vm](VolumeFluid* self, int actorGroup) {
|
||||
if (actorGroup < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Actor group must be nonnegative",
|
||||
"fluids.volume.killActor")),
|
||||
false, false);
|
||||
return script::projectResult(vm, self->killActorParticles(unsigned(actorGroup)),
|
||||
[](unsigned count) { return Value(int64_t(count)); });
|
||||
});
|
||||
cls.addFunc("applyParticleDrag", [vm](VolumeFluid* self, int particleIndex, float x, float y, float z,
|
||||
float stiffness, float damping, float seconds) {
|
||||
if (particleIndex < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Particle index must be nonnegative",
|
||||
"fluids.volume.drag")),
|
||||
false, false);
|
||||
return script::projectResult(
|
||||
vm, self->applyParticleDrag(unsigned(particleIndex), {x, y, z}, stiffness, damping, seconds));
|
||||
});
|
||||
cls.addFunc("grabContactParticles", [vm](VolumeFluid* self, int colliderLabel, float x, float y, float z, float qx,
|
||||
float qy, float qz, float qw, float distanceThreshold) {
|
||||
if (colliderLabel < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Collider label must be nonnegative",
|
||||
"fluids.volume.contactGrabber")),
|
||||
false, false);
|
||||
return script::projectResult(
|
||||
vm, self->grabContactParticles(unsigned(colliderLabel), {x, y, z}, {qx, qy, qz, qw}, distanceThreshold),
|
||||
[](unsigned count) { return Value(int64_t(count)); });
|
||||
});
|
||||
cls.addFunc("updateGrabbedParticles", [vm](VolumeFluid* self, int colliderLabel, float x, float y, float z,
|
||||
float qx, float qy, float qz, float qw) {
|
||||
if (colliderLabel < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Collider label must be nonnegative",
|
||||
"fluids.volume.contactGrabber")),
|
||||
false, false);
|
||||
return script::projectResult(vm,
|
||||
self->updateGrabbedParticles(unsigned(colliderLabel), {x, y, z}, {qx, qy, qz, qw}),
|
||||
[](unsigned count) { return Value(int64_t(count)); });
|
||||
});
|
||||
cls.addFunc("releaseGrabbedParticles", [vm](VolumeFluid* self, int colliderLabel) {
|
||||
if (colliderLabel < 0)
|
||||
return script::projectStatusResult(
|
||||
vm,
|
||||
Status::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Collider label must be nonnegative",
|
||||
"fluids.volume.contactGrabber")),
|
||||
false, false);
|
||||
return script::projectResult(vm, self->releaseGrabbedParticles(unsigned(colliderLabel)),
|
||||
[](unsigned count) { return Value(int64_t(count)); });
|
||||
});
|
||||
cls.addFunc("applyMaterialChannels",
|
||||
[vm](VolumeFluid* self) { return script::projectResult(vm, self->applyMaterialChannels()); });
|
||||
cls.addFunc("applyMaterialChannelsWithColors", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto keys = decodeVolumeFluidViscosityColors(value.value());
|
||||
if (!keys) return script::projectStatusResult(vm, keys.status(), false, false);
|
||||
return script::projectResult(vm, self->applyMaterialChannels(keys.value()));
|
||||
});
|
||||
cls.addFunc("overlapSphere", [vm](VolumeFluid* self, float x, float y, float z, float radius) {
|
||||
auto hits = self->overlap({x, y, z}, radius);
|
||||
if (!hits) return script::projectStatusResult(vm, hits.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, encodeVolumeFluidParticles(hits.value()));
|
||||
});
|
||||
cls.addFunc("paintSphere", [vm](VolumeFluid* self, float x, float y, float z, float radius, ssq::Object material) {
|
||||
auto value = script::valueFromSquirrel(material);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidMaterial(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->paint({x, y, z}, radius, decoded.value()));
|
||||
});
|
||||
cls.addFunc("overlapBox", [vm](VolumeFluid* self, float x, float y, float z, float hx, float hy, float hz, float qx,
|
||||
float qy, float qz, float qw) {
|
||||
auto hits = self->overlapBox({x, y, z}, {hx, hy, hz}, {qx, qy, qz, qw});
|
||||
if (!hits) return script::projectStatusResult(vm, hits.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, encodeVolumeFluidParticles(hits.value()));
|
||||
});
|
||||
cls.addFunc("raycast", [vm](VolumeFluid* self, float ox, float oy, float oz, float dx, float dy, float dz,
|
||||
float maxDistance, int maxHits, int phaseMask) {
|
||||
auto hits = self->raycast({ox, oy, oz}, {dx, dy, dz}, maxDistance, unsigned(maxHits), unsigned(phaseMask));
|
||||
if (!hits) return script::projectStatusResult(vm, hits.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, encodeVolumeFluidRayHits(hits.value()));
|
||||
});
|
||||
cls.addFunc("querySphere", [vm](VolumeFluid* self, float x, float y, float z, float radius, float contactOffset,
|
||||
float maxDistance, int maxHits, int phaseMask) {
|
||||
auto hits =
|
||||
self->querySphere({x, y, z}, radius, contactOffset, maxDistance, unsigned(maxHits), unsigned(phaseMask));
|
||||
if (!hits) return script::projectStatusResult(vm, hits.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true,
|
||||
encodeVolumeFluidDistanceHits(hits.value()));
|
||||
});
|
||||
cls.addFunc("queryBox",
|
||||
[vm](VolumeFluid* self, float x, float y, float z, float hx, float hy, float hz, float qx, float qy,
|
||||
float qz, float qw, float contactOffset, float maxDistance, int maxHits, int phaseMask) {
|
||||
auto hits = self->queryBox({x, y, z}, {hx, hy, hz}, {qx, qy, qz, qw}, contactOffset, maxDistance,
|
||||
unsigned(maxHits), unsigned(phaseMask));
|
||||
if (!hits) return script::projectStatusResult(vm, hits.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true,
|
||||
encodeVolumeFluidDistanceHits(hits.value()));
|
||||
});
|
||||
cls.addFunc("queryBatch", [vm](VolumeFluid* self, ssq::Object object, int maxHitsPerQuery) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto queries = decodeVolumeFluidQueries(value.value());
|
||||
if (!queries) return script::projectStatusResult(vm, queries.status(), false, false);
|
||||
auto hits = self->queryBatch(queries.value(), unsigned(maxHitsPerQuery));
|
||||
if (!hits) return script::projectStatusResult(vm, hits.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, encodeVolumeFluidQueryHits(hits.value()));
|
||||
});
|
||||
cls.addFunc("applyQueryColors", [vm](VolumeFluid* self, ssq::Object queryObject, ssq::Object colorObject, float r,
|
||||
float g, float b, float a, int maxHitsPerQuery) {
|
||||
auto queryValue = script::valueFromSquirrel(queryObject);
|
||||
if (!queryValue) return script::projectStatusResult(vm, queryValue.status(), false, false);
|
||||
auto colorValue = script::valueFromSquirrel(colorObject);
|
||||
if (!colorValue) return script::projectStatusResult(vm, colorValue.status(), false, false);
|
||||
auto queries = decodeVolumeFluidQueries(queryValue.value());
|
||||
if (!queries) return script::projectStatusResult(vm, queries.status(), false, false);
|
||||
auto colors = decodeVolumeFluidColors(colorValue.value());
|
||||
if (!colors) return script::projectStatusResult(vm, colors.status(), false, false);
|
||||
auto counts = self->applyQueryColors(queries.value(), colors.value(), {r, g, b, a}, unsigned(maxHitsPerQuery));
|
||||
if (!counts) return script::projectStatusResult(vm, counts.status(), false, false);
|
||||
Value::Array encoded;
|
||||
encoded.reserve(counts.value().size());
|
||||
for (unsigned count : counts.value()) encoded.emplace_back(int64_t(count));
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(std::move(encoded)));
|
||||
});
|
||||
cls.addFunc("applyQueryColorsPreservingOutside",
|
||||
[vm](VolumeFluid* self, ssq::Object queryObject, ssq::Object colorObject, int maxHitsPerQuery) {
|
||||
auto queryValue = script::valueFromSquirrel(queryObject);
|
||||
if (!queryValue) return script::projectStatusResult(vm, queryValue.status(), false, false);
|
||||
auto colorValue = script::valueFromSquirrel(colorObject);
|
||||
if (!colorValue) return script::projectStatusResult(vm, colorValue.status(), false, false);
|
||||
auto queries = decodeVolumeFluidQueries(queryValue.value());
|
||||
if (!queries) return script::projectStatusResult(vm, queries.status(), false, false);
|
||||
auto colors = decodeVolumeFluidColors(colorValue.value());
|
||||
if (!colors) return script::projectStatusResult(vm, colors.status(), false, false);
|
||||
auto counts = self->applyQueryColorsPreservingOutside(queries.value(), colors.value(),
|
||||
unsigned(maxHitsPerQuery));
|
||||
if (!counts) return script::projectStatusResult(vm, counts.status(), false, false);
|
||||
Value::Array encoded;
|
||||
encoded.reserve(counts.value().size());
|
||||
for (unsigned count : counts.value()) encoded.emplace_back(int64_t(count));
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(std::move(encoded)));
|
||||
});
|
||||
cls.addFunc("setSimplexes", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto simplexes = decodeVolumeFluidSimplexes(value.value());
|
||||
if (!simplexes) return script::projectStatusResult(vm, simplexes.status(), false, false);
|
||||
return script::projectResult(vm, self->setSimplexes(simplexes.value()));
|
||||
});
|
||||
cls.addFunc("querySimplexes", [vm](VolumeFluid* self, ssq::Object object, int maxHitsPerQuery) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto queries = decodeVolumeFluidQueries(value.value());
|
||||
if (!queries) return script::projectStatusResult(vm, queries.status(), false, false);
|
||||
auto hits = self->querySimplexes(queries.value(), unsigned(maxHitsPerQuery));
|
||||
if (!hits) return script::projectStatusResult(vm, hits.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true,
|
||||
encodeVolumeFluidSimplexHits(hits.value()));
|
||||
});
|
||||
cls.addFunc("setColliders", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidColliders(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->setColliders(decoded.value()));
|
||||
});
|
||||
cls.addFunc("setSdfColliders", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidSdfColliders(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->setSdfColliders(decoded.value()));
|
||||
});
|
||||
cls.addFunc("setHeightFieldColliders", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidHeightFieldColliders(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->setHeightFieldColliders(decoded.value()));
|
||||
});
|
||||
cls.addFunc("updateSdfColliderPoses", [vm](VolumeFluid* self, ssq::Object object) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidSdfPoses(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
return script::projectResult(vm, self->updateSdfColliderPoses(decoded.value()));
|
||||
});
|
||||
cls.addFunc("contacts", [vm](VolumeFluid* self) {
|
||||
return script::projectStatusResult(vm, Status::success(), true, true,
|
||||
encodeVolumeFluidContacts(self->contacts()));
|
||||
});
|
||||
cls.addFunc("emitBurst", [vm](VolumeFluid* self, ssq::Object object, int count) {
|
||||
auto value = script::valueFromSquirrel(object);
|
||||
if (!value) return script::projectStatusResult(vm, value.status(), false, false);
|
||||
auto decoded = decodeVolumeFluidEmission(value.value());
|
||||
if (!decoded) return script::projectStatusResult(vm, decoded.status(), false, false);
|
||||
auto emitted = emitVolumeFluidBurst(*self, decoded.value(), unsigned(count));
|
||||
if (!emitted) return script::projectStatusResult(vm, emitted.status(), false, false);
|
||||
return script::projectStatusResult(vm, Status::success(), true, true, Value(int64_t(emitted.value())));
|
||||
});
|
||||
cls.addFunc("getParticleCount", [](VolumeFluid* self) { return int(self->particleCount()); });
|
||||
}
|
||||
|
||||
} // namespace eve::fluids
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
namespace ssq {
|
||||
class Table;
|
||||
class Class;
|
||||
} // namespace ssq
|
||||
namespace eve::fluids {
|
||||
/** @brief Registers the script-owned volume-fluid class on the VM thread. */
|
||||
void exposeVolumeFluidType(ssq::Table& table);
|
||||
/** @brief Registers volume-fluid factories on the existing Fluids class. */
|
||||
void exposeVolumeFluidFactory(ssq::Class& cls);
|
||||
} // namespace eve::fluids
|
||||
Loaded 100 of 161 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user