@series-inc/rundot-syncplay 6.0.0-rc.3 → 6.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@
4
4
  #include <array>
5
5
  #include <cmath>
6
6
  #include <cstddef>
7
+ #include <vector>
7
8
 
8
9
  namespace rundot::kinetix::physics {
9
10
  namespace {
@@ -726,6 +727,66 @@ BuoyancyStatus calculateSubmergedGeometryWithPlane(
726
727
  return BuoyancyStatus::UnsupportedShape;
727
728
  }
728
729
 
730
+ BuoyancyStatus finishFluidForces(
731
+ const FluidBodyInput3D& body,
732
+ const FluidVolume3D& volume,
733
+ const BuoyancyVector3& gravity,
734
+ double fullVolume,
735
+ FluidForcesResult3D& output) {
736
+ if (output.submerged.volume <= 0.0) {
737
+ output.submerged = {0.0, body.position};
738
+ return BuoyancyStatus::Success;
739
+ }
740
+ const double normSq = body.orientation.x * body.orientation.x
741
+ + body.orientation.y * body.orientation.y
742
+ + body.orientation.z * body.orientation.z
743
+ + body.orientation.w * body.orientation.w;
744
+ if (!(normSq > 0.0) || !finite(normSq)) return BuoyancyStatus::InvalidInput;
745
+ const BuoyancyVector3 centerOfMassWorld = add(
746
+ body.position,
747
+ rotate(body.orientation, 1.0 / normSq, body.centerOfMass));
748
+ const BuoyancyVector3 effectiveGravity = scale(gravity, body.gravityScale);
749
+ output.buoyantForce = scale(
750
+ effectiveGravity, -volume.density * output.submerged.volume);
751
+ const BuoyancyVector3 arm = subtract(
752
+ output.submerged.center, centerOfMassWorld);
753
+ const double submergedFraction = fullVolume > 0.0
754
+ ? std::min(1.0, output.submerged.volume / fullVolume) : 0.0;
755
+ if (submergedFraction >= 0.999999999
756
+ && dot(body.centerOfMass, body.centerOfMass) < 1e-12) {
757
+ output.buoyantTorque = {};
758
+ } else {
759
+ output.buoyantTorque = cross(arm, output.buoyantForce);
760
+ }
761
+ const BuoyancyVector3 pointVelocity = add(
762
+ body.linearVelocity, cross(body.angularVelocity, arm));
763
+ output.relativeVelocity = subtract(pointVelocity, volume.flowVelocity);
764
+ const double dragCoefficient = volume.density * output.submerged.volume
765
+ * volume.linearDrag;
766
+ output.dragForce = scale(output.relativeVelocity, -dragCoefficient);
767
+ const BuoyancyVector3 dragTorqueAtPoint = cross(arm, output.dragForce);
768
+ const BuoyancyVector3 angularDamping = scale(
769
+ body.angularVelocity, -submergedFraction * volume.angularDrag);
770
+ output.dragTorque = add(dragTorqueAtPoint, angularDamping);
771
+ output.totalForce = add(output.buoyantForce, output.dragForce);
772
+ output.totalTorque = add(output.buoyantTorque, output.dragTorque);
773
+ if (!finite(output.totalForce) || !finite(output.totalTorque)) {
774
+ return BuoyancyStatus::InvalidInput;
775
+ }
776
+ const BuoyancyVector3 gravitationalForce = scale(
777
+ effectiveGravity, body.mass);
778
+ const BuoyancyVector3 netForce = add(
779
+ output.buoyantForce, gravitationalForce);
780
+ const bool equilibrium = dot(netForce, netForce) < 1e-6
781
+ && dot(output.buoyantTorque, output.buoyantTorque) < 1e-6
782
+ && dot(output.relativeVelocity, output.relativeVelocity) < 1e-4
783
+ && dot(body.angularVelocity, body.angularVelocity) < 1e-4;
784
+ output.isInEquilibrium = equilibrium;
785
+ output.shouldWake = body.sleeping
786
+ && (!equilibrium || dot(volume.flowVelocity, volume.flowVelocity) > 1e-6);
787
+ return BuoyancyStatus::Success;
788
+ }
789
+
729
790
  BuoyancyStatus calculateFluidForces(
730
791
  const FluidBodyInput3D& body,
731
792
  const FluidVolume3D& volume,
@@ -754,74 +815,216 @@ BuoyancyStatus calculateFluidForces(
754
815
  volume.bounds,
755
816
  output.submerged);
756
817
  if (geomStatus != BuoyancyStatus::Success) return geomStatus;
757
- if (output.submerged.volume <= 0.0) {
758
- output.submerged = {0.0, body.position};
759
- return BuoyancyStatus::Success;
760
- }
761
-
762
- const double normSq = body.orientation.x * body.orientation.x
763
- + body.orientation.y * body.orientation.y
764
- + body.orientation.z * body.orientation.z
765
- + body.orientation.w * body.orientation.w;
766
- if (!(normSq > 0.0) || !finite(normSq)) return BuoyancyStatus::InvalidInput;
767
- const BuoyancyVector3 centerOfMassWorld = add(
768
- body.position,
769
- rotate(body.orientation, 1.0 / normSq, body.centerOfMass));
770
-
771
- const BuoyancyVector3 effectiveGravity = scale(gravity, body.gravityScale);
772
-
773
- output.buoyantForce = scale(effectiveGravity, -volume.density * output.submerged.volume);
774
-
775
- const BuoyancyVector3 arm = subtract(output.submerged.center, centerOfMassWorld);
776
-
777
818
  const double fullVol = (body.shape == ShapeKind::Box)
778
819
  ? (8.0 * body.size.x * body.size.y * body.size.z)
779
820
  : ((4.0 / 3.0) * kPi * body.size.x * body.size.x * body.size.x);
780
- const double submergedFrac = (fullVol > 0.0)
781
- ? std::min(1.0, output.submerged.volume / fullVol)
782
- : 0.0;
821
+ return finishFluidForces(body, volume, gravity, fullVol, output);
822
+ }
783
823
 
784
- if (submergedFrac >= 0.999999999 && dot(body.centerOfMass, body.centerOfMass) < 1e-12) {
785
- output.buoyantTorque = {0.0, 0.0, 0.0};
786
- } else {
787
- output.buoyantTorque = cross(arm, output.buoyantForce);
824
+ BuoyancyStatus validateVoxelBoxes(std::span<const VoxelBoxShape3D> boxes) {
825
+ if (boxes.empty()) return BuoyancyStatus::InvalidInput;
826
+ for (std::size_t index = 0; index < boxes.size(); ++index) {
827
+ const VoxelBoxShape3D& box = boxes[index];
828
+ if (!finite(box.halfSize) || !finite(box.offset)
829
+ || !(box.halfSize.x > 0.0) || !(box.halfSize.y > 0.0)
830
+ || !(box.halfSize.z > 0.0)) return BuoyancyStatus::InvalidInput;
831
+ for (std::size_t otherIndex = 0; otherIndex < index; ++otherIndex) {
832
+ const VoxelBoxShape3D& other = boxes[otherIndex];
833
+ const bool overlap = box.offset.x - box.halfSize.x
834
+ < other.offset.x + other.halfSize.x
835
+ && other.offset.x - other.halfSize.x
836
+ < box.offset.x + box.halfSize.x
837
+ && box.offset.y - box.halfSize.y
838
+ < other.offset.y + other.halfSize.y
839
+ && other.offset.y - other.halfSize.y
840
+ < box.offset.y + box.halfSize.y
841
+ && box.offset.z - box.halfSize.z
842
+ < other.offset.z + other.halfSize.z
843
+ && other.offset.z - other.halfSize.z
844
+ < box.offset.z + box.halfSize.z;
845
+ if (overlap) return BuoyancyStatus::OverlapUnsupported;
846
+ }
788
847
  }
848
+ return BuoyancyStatus::Success;
849
+ }
789
850
 
790
- const BuoyancyVector3 pointVel = add(body.linearVelocity, cross(body.angularVelocity, arm));
791
- output.relativeVelocity = subtract(pointVel, volume.flowVelocity);
851
+ template <typename Calculate>
852
+ BuoyancyStatus calculateVoxelGeometry(
853
+ const BuoyancyVector3& position,
854
+ const BuoyancyQuaternion& orientation,
855
+ std::span<const VoxelBoxShape3D> boxes,
856
+ bool validateBoxes,
857
+ Calculate calculate,
858
+ SubmergedGeometry& output,
859
+ double& fullVolume) {
860
+ output = {};
861
+ fullVolume = 0.0;
862
+ if (validateBoxes) {
863
+ const BuoyancyStatus validation = validateVoxelBoxes(boxes);
864
+ if (validation != BuoyancyStatus::Success) return validation;
865
+ }
866
+ const double normSquared = orientation.x * orientation.x
867
+ + orientation.y * orientation.y + orientation.z * orientation.z
868
+ + orientation.w * orientation.w;
869
+ if (!(normSquared > 0.0) || !finite(normSquared) || !finite(position)) {
870
+ return BuoyancyStatus::InvalidInput;
871
+ }
872
+ BuoyancyVector3 centerMoment{};
873
+ for (const VoxelBoxShape3D& box : boxes) {
874
+ const BuoyancyVector3 center = add(position,
875
+ rotate(orientation, 1.0 / normSquared, box.offset));
876
+ SubmergedGeometry child{};
877
+ const BuoyancyStatus status = calculate(center, box.halfSize, child);
878
+ if (status != BuoyancyStatus::Success) return status;
879
+ fullVolume += 8.0 * box.halfSize.x * box.halfSize.y * box.halfSize.z;
880
+ output.volume += child.volume;
881
+ centerMoment = add(centerMoment, scale(child.center, child.volume));
882
+ }
883
+ if (!finite(fullVolume) || !finite(output.volume)) {
884
+ return BuoyancyStatus::NumericRange;
885
+ }
886
+ output.center = output.volume > 0.0
887
+ ? scale(centerMoment, 1.0 / output.volume) : position;
888
+ return finite(output.center)
889
+ ? BuoyancyStatus::Success : BuoyancyStatus::NumericRange;
890
+ }
792
891
 
793
- const double dragCoeff = volume.density * output.submerged.volume * volume.linearDrag;
794
- output.dragForce = scale(output.relativeVelocity, -dragCoeff);
892
+ BuoyancyStatus calculateVoxelBuoyancy(
893
+ const BuoyancyInput& input,
894
+ std::span<const VoxelBoxShape3D> boxes,
895
+ BuoyancyResult& output) {
896
+ output = {};
897
+ if (!(input.density > 0.0) || !finite(input.density)
898
+ || !finite(input.gravity) || !finite(input.bodyCenter)
899
+ || !finite(input.waterY)) return BuoyancyStatus::InvalidInput;
900
+ double fullVolume = 0.0;
901
+ const BuoyancyStatus status = calculateVoxelGeometry(
902
+ input.shapeCenter, input.orientation, boxes, true,
903
+ [&](const BuoyancyVector3& center, const BuoyancyVector3& halfSize,
904
+ SubmergedGeometry& child) {
905
+ return calculateSubmergedGeometry(ShapeKind::Box, center,
906
+ input.orientation, halfSize, input.waterY, child);
907
+ }, output.submerged, fullVolume);
908
+ (void) fullVolume;
909
+ if (status != BuoyancyStatus::Success) return status;
910
+ output.force = scale(input.gravity,
911
+ -input.density * output.submerged.volume);
912
+ output.torque = cross(
913
+ subtract(output.submerged.center, input.bodyCenter), output.force);
914
+ return finite(output.force) && finite(output.torque)
915
+ ? BuoyancyStatus::Success : BuoyancyStatus::NumericRange;
916
+ }
795
917
 
796
- const BuoyancyVector3 dragTorquePoint = cross(arm, output.dragForce);
918
+ BuoyancyStatus calculateVoxelFluidForcesInternal(
919
+ const FluidBodyInput3D& body,
920
+ std::span<const VoxelBoxShape3D> boxes,
921
+ const FluidVolume3D& volume,
922
+ const BuoyancyVector3& gravity,
923
+ FluidForcesResult3D& output,
924
+ bool validateBoxes) {
925
+ output = {};
926
+ if (!finite(gravity) || !finite(body.position)
927
+ || !finite(body.orientation) || !finite(body.linearVelocity)
928
+ || !finite(body.angularVelocity)) return BuoyancyStatus::InvalidInput;
929
+ const BuoyancyStatus volumeStatus = validateFluidVolume(volume);
930
+ if (volumeStatus != BuoyancyStatus::Success) return volumeStatus;
931
+ const bool filterEnabled = volume.layer != 0 || volume.mask != 0;
932
+ if (filterEnabled && ((volume.mask & body.layer) == 0
933
+ || (body.mask & volume.layer) == 0)) return BuoyancyStatus::Success;
934
+ double fullVolume = 0.0;
935
+ const BuoyancyStatus status = calculateVoxelGeometry(
936
+ body.position, body.orientation, boxes, validateBoxes,
937
+ [&](const BuoyancyVector3& center, const BuoyancyVector3& halfSize,
938
+ SubmergedGeometry& child) {
939
+ return calculateSubmergedGeometryWithPlane(ShapeKind::Box, center,
940
+ body.orientation, halfSize, volume.plane, volume.bounds, child);
941
+ }, output.submerged, fullVolume);
942
+ if (status != BuoyancyStatus::Success) return status;
943
+ return finishFluidForces(body, volume, gravity, fullVolume, output);
944
+ }
797
945
 
798
- const BuoyancyVector3 angularDamping = scale(body.angularVelocity, -submergedFrac * volume.angularDrag);
799
- output.dragTorque = add(dragTorquePoint, angularDamping);
946
+ BuoyancyStatus calculateVoxelFluidForces(
947
+ const FluidBodyInput3D& body,
948
+ std::span<const VoxelBoxShape3D> boxes,
949
+ const FluidVolume3D& volume,
950
+ const BuoyancyVector3& gravity,
951
+ FluidForcesResult3D& output) {
952
+ return calculateVoxelFluidForcesInternal(
953
+ body, boxes, volume, gravity, output, true);
954
+ }
800
955
 
801
- output.totalForce = add(output.buoyantForce, output.dragForce);
802
- output.totalTorque = add(output.buoyantTorque, output.dragTorque);
956
+ BuoyancyStatus calculateValidatedVoxelFluidForces(
957
+ const FluidBodyInput3D& body,
958
+ std::span<const VoxelBoxShape3D> boxes,
959
+ const FluidVolume3D& volume,
960
+ const BuoyancyVector3& gravity,
961
+ FluidForcesResult3D& output) {
962
+ return calculateVoxelFluidForcesInternal(
963
+ body, boxes, volume, gravity, output, false);
964
+ }
803
965
 
804
- if (!finite(output.totalForce) || !finite(output.totalTorque)) {
805
- return BuoyancyStatus::InvalidInput;
966
+ BuoyancyStatus calculateVoxelFluidStateForces(
967
+ const FluidBodyInput3D& body,
968
+ std::span<const VoxelBoxShape3D> boxes,
969
+ const FluidState3D& state,
970
+ const BuoyancyVector3& gravity,
971
+ FluidForcesResult3D& output) {
972
+ output = {};
973
+ const BuoyancyStatus stateStatus = validateFluidState(state);
974
+ if (stateStatus != BuoyancyStatus::Success) return stateStatus;
975
+ const BuoyancyStatus boxesStatus = validateVoxelBoxes(boxes);
976
+ if (boxesStatus != BuoyancyStatus::Success) return boxesStatus;
977
+ double maximumVolume = 0.0;
978
+ for (const VoxelBoxShape3D& box : boxes) {
979
+ maximumVolume += 8.0 * box.halfSize.x * box.halfSize.y * box.halfSize.z;
806
980
  }
807
-
808
- const BuoyancyVector3 gravForce = scale(effectiveGravity, body.mass);
809
- const BuoyancyVector3 netForce = add(output.buoyantForce, gravForce);
810
- const double netForceSq = dot(netForce, netForce);
811
- const double bTorqueSq = dot(output.buoyantTorque, output.buoyantTorque);
812
- const double relVelSq = dot(output.relativeVelocity, output.relativeVelocity);
813
- const double angVelSq = dot(body.angularVelocity, body.angularVelocity);
814
-
815
- const bool inEquilibrium = (netForceSq < 1e-6 && bTorqueSq < 1e-6 && relVelSq < 1e-4 && angVelSq < 1e-4);
816
- output.isInEquilibrium = inEquilibrium;
817
-
818
- if (body.sleeping) {
819
- const double flowSq = dot(volume.flowVelocity, volume.flowVelocity);
820
- output.shouldWake = (!inEquilibrium || flowSq > 1e-6);
981
+ BuoyancyVector3 centerMoment{};
982
+ BuoyancyVector3 velocityMoment{};
983
+ bool equilibrium = true;
984
+ for (std::uint32_t index = 0; index < state.volumeCount; ++index) {
985
+ FluidForcesResult3D current{};
986
+ const BuoyancyStatus status = calculateVoxelFluidForces(
987
+ body, boxes, state.volumes[index], gravity, current);
988
+ if (status != BuoyancyStatus::Success) return status;
989
+ if (state.overlapPolicy == FluidOverlapPolicy::PriorityPartition
990
+ && output.submerged.volume < maximumVolume
991
+ && current.submerged.volume > maximumVolume - output.submerged.volume) {
992
+ const double remaining = maximumVolume - output.submerged.volume;
993
+ const double factor = remaining / current.submerged.volume;
994
+ current.submerged.volume = remaining;
995
+ current.buoyantForce = scale(current.buoyantForce, factor);
996
+ current.buoyantTorque = scale(current.buoyantTorque, factor);
997
+ current.dragForce = scale(current.dragForce, factor);
998
+ current.dragTorque = scale(current.dragTorque, factor);
999
+ current.totalForce = add(current.buoyantForce, current.dragForce);
1000
+ current.totalTorque = add(current.buoyantTorque, current.dragTorque);
1001
+ }
1002
+ if (state.overlapPolicy == FluidOverlapPolicy::PriorityPartition
1003
+ && output.submerged.volume >= maximumVolume) continue;
1004
+ output.buoyantForce = add(output.buoyantForce, current.buoyantForce);
1005
+ output.buoyantTorque = add(output.buoyantTorque, current.buoyantTorque);
1006
+ output.dragForce = add(output.dragForce, current.dragForce);
1007
+ output.dragTorque = add(output.dragTorque, current.dragTorque);
1008
+ output.totalForce = add(output.totalForce, current.totalForce);
1009
+ output.totalTorque = add(output.totalTorque, current.totalTorque);
1010
+ centerMoment = add(centerMoment,
1011
+ scale(current.submerged.center, current.submerged.volume));
1012
+ velocityMoment = add(velocityMoment,
1013
+ scale(current.relativeVelocity, current.submerged.volume));
1014
+ output.submerged.volume += current.submerged.volume;
1015
+ output.shouldWake = output.shouldWake || current.shouldWake;
1016
+ if (current.submerged.volume > 0.0 && !current.isInEquilibrium) {
1017
+ equilibrium = false;
1018
+ }
1019
+ }
1020
+ if (output.submerged.volume > 0.0) {
1021
+ const double inverseVolume = 1.0 / output.submerged.volume;
1022
+ output.submerged.center = scale(centerMoment, inverseVolume);
1023
+ output.relativeVelocity = scale(velocityMoment, inverseVolume);
1024
+ output.isInEquilibrium = equilibrium;
821
1025
  } else {
822
- output.shouldWake = false;
1026
+ output.submerged.center = body.position;
823
1027
  }
824
-
825
1028
  return BuoyancyStatus::Success;
826
1029
  }
827
1030
 
@@ -924,6 +1127,51 @@ BuoyancyStatus stepBodyInFluids(
924
1127
 
925
1128
  } // namespace rundot::kinetix::physics
926
1129
 
1130
+ namespace {
1131
+
1132
+ bool parseVoxelBoxes(
1133
+ const double* values,
1134
+ std::uint32_t valueCount,
1135
+ std::vector<rundot::kinetix::physics::VoxelBoxShape3D>& output) {
1136
+ using rundot::kinetix::physics::VoxelBoxShape3D;
1137
+ if (values == nullptr || valueCount == 0 || valueCount % 6 != 0) return false;
1138
+ output.resize(valueCount / 6);
1139
+ for (std::size_t index = 0; index < output.size(); ++index) {
1140
+ const double* record = values + index * 6;
1141
+ for (std::size_t field = 0; field < 6; ++field) {
1142
+ if (!std::isfinite(record[field])) return false;
1143
+ }
1144
+ output[index] = VoxelBoxShape3D{
1145
+ {record[0], record[1], record[2]},
1146
+ {record[3], record[4], record[5]},
1147
+ };
1148
+ }
1149
+ return true;
1150
+ }
1151
+
1152
+ void writeFluidResult(
1153
+ const rundot::kinetix::physics::FluidForcesResult3D& result,
1154
+ double* output) {
1155
+ const double values[rundot::kinetix::physics::kFluidForcesOutputStride] = {
1156
+ result.submerged.volume,
1157
+ result.submerged.center.x, result.submerged.center.y,
1158
+ result.submerged.center.z,
1159
+ result.buoyantForce.x, result.buoyantForce.y, result.buoyantForce.z,
1160
+ result.buoyantTorque.x, result.buoyantTorque.y, result.buoyantTorque.z,
1161
+ result.dragForce.x, result.dragForce.y, result.dragForce.z,
1162
+ result.dragTorque.x, result.dragTorque.y, result.dragTorque.z,
1163
+ result.totalForce.x, result.totalForce.y, result.totalForce.z,
1164
+ result.totalTorque.x, result.totalTorque.y, result.totalTorque.z,
1165
+ result.relativeVelocity.x, result.relativeVelocity.y,
1166
+ result.relativeVelocity.z,
1167
+ result.shouldWake ? 1.0 : 0.0,
1168
+ result.isInEquilibrium ? 1.0 : 0.0,
1169
+ };
1170
+ std::copy(std::begin(values), std::end(values), output);
1171
+ }
1172
+
1173
+ } // namespace
1174
+
927
1175
  extern "C" std::int32_t rundot_kinetix_physics_buoyancy(
928
1176
  const double* input,
929
1177
  std::uint32_t input_values,
@@ -983,6 +1231,64 @@ extern "C" std::int32_t rundot_kinetix_physics_buoyancy(
983
1231
  return static_cast<std::int32_t>(status);
984
1232
  }
985
1233
 
1234
+ extern "C" std::int32_t rundot_kinetix_physics_voxel_buoyancy(
1235
+ const double* input,
1236
+ std::uint32_t input_values,
1237
+ const double* children,
1238
+ std::uint32_t child_values,
1239
+ double* output,
1240
+ std::uint32_t output_values) {
1241
+ using namespace rundot::kinetix::physics;
1242
+ if (output == nullptr || output_values < kBuoyancyOutputStride) {
1243
+ return static_cast<std::int32_t>(BuoyancyStatus::OutputTooSmall);
1244
+ }
1245
+ if (input == nullptr || input_values != kBuoyancyInputStride) {
1246
+ return static_cast<std::int32_t>(BuoyancyStatus::InvalidInput);
1247
+ }
1248
+ for (std::uint32_t index = 0; index < input_values; ++index) {
1249
+ if (!std::isfinite(input[index])) {
1250
+ return static_cast<std::int32_t>(BuoyancyStatus::InvalidInput);
1251
+ }
1252
+ }
1253
+ std::vector<VoxelBoxShape3D> boxes;
1254
+ if (!parseVoxelBoxes(children, child_values, boxes)) {
1255
+ return static_cast<std::int32_t>(BuoyancyStatus::InvalidInput);
1256
+ }
1257
+ BuoyancyInput parsed{
1258
+ ShapeKind::Compound,
1259
+ {input[1], input[2], input[3]},
1260
+ {input[4], input[5], input[6], input[7]},
1261
+ {},
1262
+ input[11],
1263
+ input[12],
1264
+ {input[13], input[14], input[15]},
1265
+ {input[16], input[17], input[18]},
1266
+ };
1267
+ const double normSquared = parsed.orientation.x * parsed.orientation.x
1268
+ + parsed.orientation.y * parsed.orientation.y
1269
+ + parsed.orientation.z * parsed.orientation.z
1270
+ + parsed.orientation.w * parsed.orientation.w;
1271
+ if (!(normSquared > 0.0) || !std::isfinite(normSquared)) {
1272
+ return static_cast<std::int32_t>(BuoyancyStatus::InvalidInput);
1273
+ }
1274
+ parsed.bodyCenter = add(parsed.shapeCenter,
1275
+ rotate(parsed.orientation, 1.0 / normSquared, parsed.bodyCenter));
1276
+ BuoyancyResult result{};
1277
+ const BuoyancyStatus status = calculateVoxelBuoyancy(parsed, boxes, result);
1278
+ if (status != BuoyancyStatus::Success) {
1279
+ return static_cast<std::int32_t>(status);
1280
+ }
1281
+ const double values[kBuoyancyOutputStride] = {
1282
+ result.submerged.volume,
1283
+ result.submerged.center.x, result.submerged.center.y,
1284
+ result.submerged.center.z,
1285
+ result.force.x, result.force.y, result.force.z,
1286
+ result.torque.x, result.torque.y, result.torque.z,
1287
+ };
1288
+ std::copy(std::begin(values), std::end(values), output);
1289
+ return static_cast<std::int32_t>(BuoyancyStatus::Success);
1290
+ }
1291
+
986
1292
  extern "C" std::uint32_t rundot_kinetix_physics_buoyancy_abi_version() {
987
1293
  return 1;
988
1294
  }
@@ -1091,6 +1397,77 @@ extern "C" std::int32_t rundot_kinetix_physics_fluid_forces(
1091
1397
  return static_cast<std::int32_t>(BuoyancyStatus::Success);
1092
1398
  }
1093
1399
 
1400
+ extern "C" std::int32_t rundot_kinetix_physics_voxel_fluid_forces(
1401
+ const double* input,
1402
+ std::uint32_t input_values,
1403
+ const double* children,
1404
+ std::uint32_t child_values,
1405
+ double* output,
1406
+ std::uint32_t output_values) {
1407
+ using namespace rundot::kinetix::physics;
1408
+ if (output == nullptr || output_values < kFluidForcesOutputStride) {
1409
+ return static_cast<std::int32_t>(BuoyancyStatus::OutputTooSmall);
1410
+ }
1411
+ if (input == nullptr || input_values != kFluidForcesInputStride) {
1412
+ return static_cast<std::int32_t>(BuoyancyStatus::InvalidInput);
1413
+ }
1414
+ for (std::uint32_t index = 0; index < input_values; ++index) {
1415
+ if (!std::isfinite(input[index])) {
1416
+ return static_cast<std::int32_t>(BuoyancyStatus::InvalidInput);
1417
+ }
1418
+ }
1419
+ std::vector<VoxelBoxShape3D> boxes;
1420
+ if (!parseVoxelBoxes(children, child_values, boxes)) {
1421
+ return static_cast<std::int32_t>(BuoyancyStatus::InvalidInput);
1422
+ }
1423
+ const auto u32 = [](double value, std::uint32_t& result) {
1424
+ if (value < 0.0 || value > 4294967295.0
1425
+ || std::floor(value) != value) return false;
1426
+ result = static_cast<std::uint32_t>(value);
1427
+ return true;
1428
+ };
1429
+ std::uint32_t ignoredShape = 0;
1430
+ std::uint32_t booleanValue = 0;
1431
+ FluidBodyInput3D body{};
1432
+ FluidVolume3D volume{};
1433
+ if (!u32(input[0], ignoredShape)
1434
+ || !u32(input[25], booleanValue) || booleanValue > 1
1435
+ || !u32(input[26], body.layer) || !u32(input[27], body.mask)
1436
+ || !u32(input[28], volume.id)
1437
+ || !u32(input[35], booleanValue) || booleanValue > 1
1438
+ || !u32(input[46], volume.layer) || !u32(input[47], volume.mask)) {
1439
+ return static_cast<std::int32_t>(BuoyancyStatus::InvalidInput);
1440
+ }
1441
+ body.shape = ShapeKind::Compound;
1442
+ body.position = {input[1], input[2], input[3]};
1443
+ body.orientation = {input[4], input[5], input[6], input[7]};
1444
+ body.centerOfMass = {input[11], input[12], input[13]};
1445
+ body.linearVelocity = {input[14], input[15], input[16]};
1446
+ body.angularVelocity = {input[17], input[18], input[19]};
1447
+ body.mass = input[20];
1448
+ body.inertiaDiag = {input[21], input[22], input[23]};
1449
+ body.gravityScale = input[24];
1450
+ body.sleeping = input[25] != 0.0;
1451
+ volume.bounds = {
1452
+ input[29], input[30], input[31], input[32], input[33], input[34]};
1453
+ volume.plane.enabled = input[35] != 0.0;
1454
+ volume.plane.normal = {input[36], input[37], input[38]};
1455
+ volume.plane.offset = input[39];
1456
+ volume.density = input[40];
1457
+ volume.linearDrag = input[41];
1458
+ volume.angularDrag = input[42];
1459
+ volume.flowVelocity = {input[43], input[44], input[45]};
1460
+ const BuoyancyVector3 gravity{input[48], input[49], input[50]};
1461
+ FluidForcesResult3D result{};
1462
+ const BuoyancyStatus status = calculateVoxelFluidForces(
1463
+ body, boxes, volume, gravity, result);
1464
+ if (status != BuoyancyStatus::Success) {
1465
+ return static_cast<std::int32_t>(status);
1466
+ }
1467
+ writeFluidResult(result, output);
1468
+ return static_cast<std::int32_t>(BuoyancyStatus::Success);
1469
+ }
1470
+
1094
1471
  extern "C" std::int32_t rundot_kinetix_physics_fluid_state_forces(
1095
1472
  const double* input,
1096
1473
  std::uint32_t input_values,
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
 
3
3
  #include <cstdint>
4
+ #include <span>
4
5
 
5
6
  #include "physics.hpp"
6
7
 
@@ -129,6 +130,11 @@ struct FluidBodyInput3D {
129
130
  std::uint32_t mask = 1;
130
131
  };
131
132
 
133
+ struct VoxelBoxShape3D {
134
+ BuoyancyVector3 halfSize{};
135
+ BuoyancyVector3 offset{};
136
+ };
137
+
132
138
  struct FluidForcesResult3D {
133
139
  SubmergedGeometry submerged{};
134
140
  BuoyancyVector3 buoyantForce{};
@@ -161,6 +167,34 @@ BuoyancyStatus calculateFluidForces(
161
167
  const BuoyancyVector3& gravity,
162
168
  FluidForcesResult3D& output);
163
169
 
170
+ BuoyancyStatus validateVoxelBoxes(std::span<const VoxelBoxShape3D> boxes);
171
+
172
+ BuoyancyStatus calculateVoxelBuoyancy(
173
+ const BuoyancyInput& input,
174
+ std::span<const VoxelBoxShape3D> boxes,
175
+ BuoyancyResult& output);
176
+
177
+ BuoyancyStatus calculateVoxelFluidForces(
178
+ const FluidBodyInput3D& body,
179
+ std::span<const VoxelBoxShape3D> boxes,
180
+ const FluidVolume3D& volume,
181
+ const BuoyancyVector3& gravity,
182
+ FluidForcesResult3D& output);
183
+
184
+ BuoyancyStatus calculateValidatedVoxelFluidForces(
185
+ const FluidBodyInput3D& body,
186
+ std::span<const VoxelBoxShape3D> boxes,
187
+ const FluidVolume3D& volume,
188
+ const BuoyancyVector3& gravity,
189
+ FluidForcesResult3D& output);
190
+
191
+ BuoyancyStatus calculateVoxelFluidStateForces(
192
+ const FluidBodyInput3D& body,
193
+ std::span<const VoxelBoxShape3D> boxes,
194
+ const FluidState3D& state,
195
+ const BuoyancyVector3& gravity,
196
+ FluidForcesResult3D& output);
197
+
164
198
  BuoyancyStatus stepBodyInFluids(
165
199
  FluidBodyInput3D& body,
166
200
  const FluidState3D& state,
@@ -195,6 +229,20 @@ extern "C" std::int32_t rundot_kinetix_physics_fluid_state_forces(
195
229
  std::uint32_t input_values,
196
230
  double* output,
197
231
  std::uint32_t output_values);
232
+ extern "C" std::int32_t rundot_kinetix_physics_voxel_buoyancy(
233
+ const double* input,
234
+ std::uint32_t input_values,
235
+ const double* children,
236
+ std::uint32_t child_values,
237
+ double* output,
238
+ std::uint32_t output_values);
239
+ extern "C" std::int32_t rundot_kinetix_physics_voxel_fluid_forces(
240
+ const double* input,
241
+ std::uint32_t input_values,
242
+ const double* children,
243
+ std::uint32_t child_values,
244
+ double* output,
245
+ std::uint32_t output_values);
198
246
  extern "C" std::int32_t rundot_kinetix_physics_validate_fluid_volume(
199
247
  const rundot::kinetix::physics::FluidVolume3D* volume);
200
248
  extern "C" std::int32_t rundot_kinetix_physics_validate_fluid_state(
@@ -103,7 +103,8 @@ bool validGeometry(std::span<const double> geometry, ShapeKind kind,
103
103
  if (primitive && !validPrimitive(childKind, record + 4)) return false;
104
104
  if (format == 4) continue;
105
105
  if (!nonNegativeInteger(record[13], count) || !nonNegativeInteger(record[14], count)
106
- || !nonNegativeInteger(record[15], 5) || record[16] != 0 || record[17] != children) return false;
106
+ || !nonNegativeInteger(record[15], 5) || !nonNegativeInteger(record[16], 1)
107
+ || record[16] != values[16] || record[17] != children) return false;
107
108
  const auto childOffset = static_cast<std::uint64_t>(record[13]);
108
109
  const auto childCount = static_cast<std::uint64_t>(record[14]);
109
110
  if (primitive) {
@@ -115,6 +116,24 @@ bool validGeometry(std::span<const double> geometry, ShapeKind kind,
115
116
  static_cast<std::uint32_t>(record[15]), depth + 1)) return false;
116
117
  payloadEnd = childOffset + childCount;
117
118
  }
119
+ if (format == 5 && values[16] == 1) {
120
+ constexpr std::uint64_t recordStride = 18;
121
+ for (std::uint64_t child = 0; child < children; ++child) {
122
+ const double* first = values + child * recordStride;
123
+ if (first[0] != static_cast<double>(ShapeKind::Box)
124
+ || first[13] != 0 || first[14] != 0 || first[15] != 0) return false;
125
+ for (std::uint64_t other = 0; other < child; ++other) {
126
+ const double* second = values + other * recordStride;
127
+ const bool overlap = first[1] - first[4] < second[1] + second[4]
128
+ && second[1] - second[4] < first[1] + first[4]
129
+ && first[2] - first[5] < second[2] + second[5]
130
+ && second[2] - second[5] < first[2] + first[5]
131
+ && first[3] - first[6] < second[3] + second[6]
132
+ && second[3] - second[6] < first[3] + first[6];
133
+ if (overlap) return false;
134
+ }
135
+ }
136
+ }
118
137
  return true;
119
138
  }
120
139