feat(fluids): add Fluid3D simulation and rendering

This commit is contained in:
西风逍遥游 committed 2026-09-14 23:28:26 +08:00
1 parent dfc9f39b08
commit 92109ae643
161 files changed
+24955 -350

No files matched your search

+3 -2
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+27
View File
@@ -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;
}
+866
View File
@@ -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
+172
View File
@@ -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
+82
View File
@@ -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;
}
+50
View File
@@ -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;
}
+39
View File
@@ -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;
}
+50
View File
@@ -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);
}
+27
View File
@@ -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;
+24
View File
@@ -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;
}
+6
View File
@@ -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;
}
+29
View File
@@ -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";
+21
View File
@@ -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");
+88
View File
@@ -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");
+27
View File
@@ -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");
+20
View File
@@ -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";
+18
View File
@@ -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";
+1
View File
@@ -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
+21
View File
@@ -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";
+21
View File
@@ -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;
}
+42
View File
@@ -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);
}
+43
View File
@@ -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;
}
+41
View File
@@ -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.
+78
View File
@@ -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'.
+65
View File
@@ -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;
}
+17
View File
@@ -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;
+16
View File
@@ -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;
+22
View File
@@ -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]));
}
+37
View File
@@ -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};
}
+26
View File
@@ -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;
}
+699
View File
@@ -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);
};
+87
View File
@@ -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;
}
+98
View File
@@ -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()};
}
+35
View File
@@ -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;
}
+24
View File
@@ -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;
}
+25
View File
@@ -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";
+20
View File
@@ -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;
}
+14
View File
@@ -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";
+242
View File
@@ -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;
}
+11
View File
@@ -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";
}
+23
View File
@@ -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();
+19
View File
@@ -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;
}
+32
View File
@@ -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";
}
+20
View File
@@ -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";
+9
View File
@@ -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;
+11
View File
@@ -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;
}
+28
View File
@@ -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;
}
+93
View File
@@ -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;
+53
View File
@@ -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);
}
+15
View File
@@ -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";
+6
View File
@@ -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]];
}
+33
View File
@@ -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;
}
}
+40
View File
@@ -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;
}
+20
View File
@@ -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;
}
+51 -31
View File
@@ -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";
+53 -36
View File
@@ -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];
}
}
+19 -4
View File
@@ -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).
*/
+27 -13
View File
@@ -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;
+201 -31
View File
@@ -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";
+80
View File
@@ -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
+448 -23
View File
@@ -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
+208 -3
View File
@@ -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);
+8
View File
@@ -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;
+877
View File
@@ -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
+816
View File
@@ -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
+11
View File
@@ -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