1use super::functions::*;
6use crate::particle::SoftParticle;
7use oxiphysics_core::math::{Real, Vec3};
8
9#[derive(Debug, Clone)]
11pub struct CollisionConstraint {
12 pub particle_index: usize,
14 pub normal: Vec3,
16 pub point_on_plane: Vec3,
18}
19impl CollisionConstraint {
20 pub fn new(particle_index: usize, normal: Vec3, point_on_plane: Vec3) -> Self {
22 Self {
23 particle_index,
24 normal,
25 point_on_plane,
26 }
27 }
28}
29#[derive(Debug, Clone)]
35pub struct LraConstraint {
36 pub particle: usize,
38 pub anchor: Vec3,
40 pub max_distance: Real,
42 pub compliance: Real,
44 pub(super) lambda: Real,
46}
47impl LraConstraint {
48 pub fn new(particle: usize, anchor: Vec3, max_distance: Real, compliance: Real) -> Self {
50 Self {
51 particle,
52 anchor,
53 max_distance,
54 compliance,
55 lambda: 0.0,
56 }
57 }
58 pub fn from_particle(
60 particle: usize,
61 particles: &[SoftParticle],
62 anchor: Vec3,
63 compliance: Real,
64 ) -> Self {
65 let dist = (particles[particle].position - anchor).norm();
66 Self::new(particle, anchor, dist, compliance)
67 }
68 pub fn reset_lambda(&mut self) {
70 self.lambda = 0.0;
71 }
72 pub fn constraint_value(&self, particles: &[SoftParticle]) -> Real {
74 let dist = (particles[self.particle].position - self.anchor).norm();
75 (dist - self.max_distance).max(0.0)
76 }
77}
78#[derive(Debug, Clone)]
83pub struct SurfaceTensionConstraint {
84 pub indices: [usize; 3],
86 pub rest_area: Real,
88 pub gamma: Real,
90 pub compliance: Real,
92 pub(super) lambda: Real,
94}
95impl SurfaceTensionConstraint {
96 pub fn from_particles(
98 indices: [usize; 3],
99 particles: &[SoftParticle],
100 gamma: Real,
101 compliance: Real,
102 ) -> Self {
103 let p0 = particles[indices[0]].position;
104 let p1 = particles[indices[1]].position;
105 let p2 = particles[indices[2]].position;
106 let rest_area = 0.5 * (p1 - p0).cross(&(p2 - p0)).norm();
107 Self {
108 indices,
109 rest_area,
110 gamma,
111 compliance,
112 lambda: 0.0,
113 }
114 }
115 pub fn reset_lambda(&mut self) {
117 self.lambda = 0.0;
118 }
119 pub fn current_area(p0: &Vec3, p1: &Vec3, p2: &Vec3) -> Real {
121 0.5 * (p1 - p0).cross(&(p2 - p0)).norm()
122 }
123 pub fn surface_energy(&self, particles: &[SoftParticle]) -> Real {
125 let [i0, i1, i2] = self.indices;
126 let area = Self::current_area(
127 &particles[i0].position,
128 &particles[i1].position,
129 &particles[i2].position,
130 );
131 self.gamma * (area - self.rest_area)
132 }
133}
134#[derive(Debug, Clone)]
139pub struct ShapeMatchingConstraint {
140 pub indices: Vec<usize>,
142 pub rest_relative: Vec<Vec3>,
144 pub rest_com: Vec3,
146 pub stiffness: Real,
148}
149impl ShapeMatchingConstraint {
150 pub fn from_particles(
152 indices: Vec<usize>,
153 particles: &[SoftParticle],
154 stiffness: Real,
155 ) -> Self {
156 let n = indices.len();
157 if n == 0 {
158 return Self {
159 indices,
160 rest_relative: Vec::new(),
161 rest_com: Vec3::zeros(),
162 stiffness,
163 };
164 }
165 let total_inv_mass: Real = indices
166 .iter()
167 .map(|&i| {
168 if particles[i].inverse_mass > 0.0 {
169 1.0 / particles[i].inverse_mass
170 } else {
171 1e6
172 }
173 })
174 .sum();
175 let mut com = Vec3::zeros();
176 for &i in &indices {
177 let mass = if particles[i].inverse_mass > 0.0 {
178 1.0 / particles[i].inverse_mass
179 } else {
180 1e6
181 };
182 com += particles[i].position * mass;
183 }
184 com /= total_inv_mass;
185 let rest_relative: Vec<Vec3> = indices
186 .iter()
187 .map(|&i| particles[i].position - com)
188 .collect();
189 Self {
190 indices,
191 rest_relative,
192 rest_com: com,
193 stiffness,
194 }
195 }
196 pub(super) fn current_com(&self, particles: &[SoftParticle]) -> Vec3 {
198 let n = self.indices.len();
199 if n == 0 {
200 return Vec3::zeros();
201 }
202 let mut total_mass = 0.0_f64;
203 let mut com = Vec3::zeros();
204 for &i in &self.indices {
205 let mass = if particles[i].inverse_mass > 0.0 {
206 1.0 / particles[i].inverse_mass
207 } else {
208 1e6
209 };
210 com += particles[i].position * mass;
211 total_mass += mass;
212 }
213 if total_mass > 1e-14 {
214 com / total_mass
215 } else {
216 Vec3::zeros()
217 }
218 }
219 pub(super) fn compute_rotation(&self, particles: &[SoftParticle]) -> [Real; 9] {
224 let com = self.current_com(particles);
225 let mut a = [0.0_f64; 9];
226 for (k, &idx) in self.indices.iter().enumerate() {
227 let mass = if particles[idx].inverse_mass > 0.0 {
228 1.0 / particles[idx].inverse_mass
229 } else {
230 1e6
231 };
232 let p = particles[idx].position - com;
233 let q = &self.rest_relative[k];
234 a[0] += mass * p.x * q.x;
235 a[1] += mass * p.x * q.y;
236 a[2] += mass * p.x * q.z;
237 a[3] += mass * p.y * q.x;
238 a[4] += mass * p.y * q.y;
239 a[5] += mass * p.y * q.z;
240 a[6] += mass * p.z * q.x;
241 a[7] += mass * p.z * q.y;
242 a[8] += mass * p.z * q.z;
243 }
244 let col0 = Vec3::new(a[0], a[3], a[6]);
245 let col1_raw = Vec3::new(a[1], a[4], a[7]);
246 let col2_raw = Vec3::new(a[2], a[5], a[8]);
247 let len0 = col0.norm();
248 if len0 < 1e-14 {
249 return [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
250 }
251 let e0 = col0 / len0;
252 let proj1 = col1_raw - e0 * e0.dot(&col1_raw);
253 let len1 = proj1.norm();
254 if len1 < 1e-14 {
255 return [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
256 }
257 let e1 = proj1 / len1;
258 let e2_raw = col2_raw - e0 * e0.dot(&col2_raw) - e1 * e1.dot(&col2_raw);
259 let len2 = e2_raw.norm();
260 if len2 < 1e-14 {
261 return [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
262 }
263 let e2 = e2_raw / len2;
264 let det_sign = e0.dot(&e1.cross(&e2));
265 let e2 = if det_sign < 0.0 { -e2 } else { e2 };
266 [e0.x, e1.x, e2.x, e0.y, e1.y, e2.y, e0.z, e1.z, e2.z]
267 }
268}
269#[derive(Debug, Clone)]
274pub struct StrainLimitingConstraint {
275 pub indices: [usize; 3],
277 pub max_stretch: Real,
279 pub min_compression: Real,
281 pub(super) inv_rest: [Real; 4],
283 pub compliance: Real,
285 pub(super) lambda: Real,
287}
288impl StrainLimitingConstraint {
289 pub fn from_particles(
291 indices: [usize; 3],
292 particles: &[SoftParticle],
293 max_stretch: Real,
294 min_compression: Real,
295 compliance: Real,
296 ) -> Self {
297 let p0 = particles[indices[0]].position;
298 let p1 = particles[indices[1]].position;
299 let p2 = particles[indices[2]].position;
300 let inv_rest = Self::compute_inv_rest(&p0, &p1, &p2);
301 Self {
302 indices,
303 max_stretch,
304 min_compression,
305 inv_rest,
306 compliance,
307 lambda: 0.0,
308 }
309 }
310 pub fn reset_lambda(&mut self) {
312 self.lambda = 0.0;
313 }
314 fn compute_inv_rest(p0: &Vec3, p1: &Vec3, p2: &Vec3) -> [Real; 4] {
316 let d1 = p1 - p0;
317 let d2 = p2 - p0;
318 let e1 = d1;
319 let e1_len = e1.norm();
320 if e1_len < 1e-14 {
321 return [1.0, 0.0, 0.0, 1.0];
322 }
323 let u = e1 / e1_len;
324 let normal = d1.cross(&d2);
325 let n_len = normal.norm();
326 if n_len < 1e-14 {
327 return [1.0, 0.0, 0.0, 1.0];
328 }
329 let v = normal.cross(&d1).normalize();
330 let a = d1.dot(&u);
331 let b = d1.dot(&v);
332 let c = d2.dot(&u);
333 let d = d2.dot(&v);
334 let det = a * d - b * c;
335 if det.abs() < 1e-14 {
336 return [1.0, 0.0, 0.0, 1.0];
337 }
338 [d / det, -c / det, -b / det, a / det]
339 }
340}
341#[derive(Debug, Clone)]
349pub struct NeoHookeanConstraint {
350 pub indices: [usize; 4],
352 pub mu: Real,
354 pub lambda_lame: Real,
356 pub compliance: Real,
358 pub(super) dm_inv: [Real; 9],
360 pub rest_volume: Real,
362 pub(super) lambda_dev: Real,
364 pub(super) lambda_vol: Real,
366}
367impl NeoHookeanConstraint {
368 pub fn from_particles(
370 indices: [usize; 4],
371 particles: &[SoftParticle],
372 mu: Real,
373 lambda_lame: Real,
374 compliance: Real,
375 ) -> Self {
376 let p0 = particles[indices[0]].position;
377 let p1 = particles[indices[1]].position;
378 let p2 = particles[indices[2]].position;
379 let p3 = particles[indices[3]].position;
380 let dm = Self::compute_shape_matrix(&p0, &p1, &p2, &p3);
381 let dm_inv = mat3_inverse(dm).unwrap_or([0.0; 9]);
382 let rest_volume = VolumeConstraint::compute_tet_volume(&p0, &p1, &p2, &p3).abs();
383 Self {
384 indices,
385 mu,
386 lambda_lame,
387 compliance,
388 dm_inv,
389 rest_volume,
390 lambda_dev: 0.0,
391 lambda_vol: 0.0,
392 }
393 }
394 pub fn reset_lambda(&mut self) {
396 self.lambda_dev = 0.0;
397 self.lambda_vol = 0.0;
398 }
399 pub fn deformation_gradient(&self, particles: &[SoftParticle]) -> [Real; 9] {
401 let [i0, i1, i2, i3] = self.indices;
402 let p0 = particles[i0].position;
403 let p1 = particles[i1].position;
404 let p2 = particles[i2].position;
405 let p3 = particles[i3].position;
406 let ds = Self::compute_shape_matrix(&p0, &p1, &p2, &p3);
407 mat3_mul(ds, self.dm_inv)
408 }
409 pub fn compute_i1(f: [Real; 9]) -> Real {
411 f.iter().map(|&x| x * x).sum()
412 }
413 pub fn compute_j(f: [Real; 9]) -> Real {
415 mat3_det(f)
416 }
417 fn compute_shape_matrix(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3) -> [Real; 9] {
418 let c0 = *p1 - *p0;
419 let c1 = *p2 - *p0;
420 let c2 = *p3 - *p0;
421 [c0.x, c0.y, c0.z, c1.x, c1.y, c1.z, c2.x, c2.y, c2.z]
422 }
423}
424#[derive(Debug, Clone)]
428pub struct AreaConstraint {
429 pub indices: [usize; 3],
431 pub rest_area: Real,
433 pub compliance: Real,
435 pub(super) lambda: Real,
437}
438impl AreaConstraint {
439 pub fn new(indices: [usize; 3], rest_area: Real, compliance: Real) -> Self {
441 Self {
442 indices,
443 rest_area,
444 compliance,
445 lambda: 0.0,
446 }
447 }
448 pub fn from_particles(
450 indices: [usize; 3],
451 particles: &[SoftParticle],
452 compliance: Real,
453 ) -> Self {
454 let area = Self::compute_triangle_area(
455 &particles[indices[0]].position,
456 &particles[indices[1]].position,
457 &particles[indices[2]].position,
458 );
459 Self::new(indices, area, compliance)
460 }
461 pub fn reset_lambda(&mut self) {
463 self.lambda = 0.0;
464 }
465 pub(super) fn compute_triangle_area(p0: &Vec3, p1: &Vec3, p2: &Vec3) -> Real {
467 let e1 = p1 - p0;
468 let e2 = p2 - p0;
469 0.5 * e1.cross(&e2).norm()
470 }
471}
472#[derive(Debug, Clone)]
477pub struct RigidBindingConstraint {
478 pub particle: usize,
480 pub target: Vec3,
482 pub compliance: Real,
484 pub(super) lambda: Real,
486}
487impl RigidBindingConstraint {
488 pub fn new(particle: usize, target: Vec3, compliance: Real) -> Self {
490 Self {
491 particle,
492 target,
493 compliance,
494 lambda: 0.0,
495 }
496 }
497 pub fn reset_lambda(&mut self) {
499 self.lambda = 0.0;
500 }
501 pub fn set_target(&mut self, target: Vec3) {
503 self.target = target;
504 }
505 pub fn residual(&self, particles: &[SoftParticle]) -> Real {
507 (particles[self.particle].position - self.target).norm()
508 }
509}
510#[derive(Debug, Clone)]
515pub struct MotorConstraint {
516 pub i: usize,
518 pub j: usize,
520 pub target_length: Real,
522 pub rest_length: Real,
524 pub min_ratio: Real,
526 pub compliance: Real,
528 pub actuation: Real,
530 pub(super) lambda: Real,
532}
533impl MotorConstraint {
534 pub fn new(i: usize, j: usize, rest_length: Real, min_ratio: Real, compliance: Real) -> Self {
536 Self {
537 i,
538 j,
539 target_length: rest_length,
540 rest_length,
541 min_ratio,
542 compliance,
543 actuation: 0.0,
544 lambda: 0.0,
545 }
546 }
547 pub fn from_particles(
549 i: usize,
550 j: usize,
551 particles: &[SoftParticle],
552 min_ratio: Real,
553 compliance: Real,
554 ) -> Self {
555 let rest = (particles[i].position - particles[j].position).norm();
556 Self::new(i, j, rest, min_ratio, compliance)
557 }
558 pub fn reset_lambda(&mut self) {
560 self.lambda = 0.0;
561 }
562 pub fn set_actuation(&mut self, actuation: Real) {
564 self.actuation = actuation.clamp(0.0, 1.0);
565 let min_len = self.rest_length * self.min_ratio;
566 self.target_length = self.rest_length - (self.rest_length - min_len) * self.actuation;
567 }
568 pub fn contraction_ratio(&self) -> Real {
570 if self.rest_length < 1e-12 {
571 return 1.0;
572 }
573 self.target_length / self.rest_length
574 }
575}
576#[derive(Debug, Clone)]
580pub struct VolumeConstraint {
581 pub indices: [usize; 4],
583 pub rest_volume: Real,
585 pub compliance: Real,
587 pub(super) lambda: Real,
589}
590impl VolumeConstraint {
591 pub fn new(indices: [usize; 4], rest_volume: Real, compliance: Real) -> Self {
593 Self {
594 indices,
595 rest_volume,
596 compliance,
597 lambda: 0.0,
598 }
599 }
600 pub fn from_particles(
603 indices: [usize; 4],
604 particles: &[SoftParticle],
605 compliance: Real,
606 ) -> Self {
607 let vol = Self::compute_tet_volume(
608 &particles[indices[0]].position,
609 &particles[indices[1]].position,
610 &particles[indices[2]].position,
611 &particles[indices[3]].position,
612 );
613 Self::new(indices, vol, compliance)
614 }
615 pub fn reset_lambda(&mut self) {
617 self.lambda = 0.0;
618 }
619 pub fn compute_tet_volume(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3) -> Real {
621 let a = p1 - p0;
622 let b = p2 - p0;
623 let c = p3 - p0;
624 a.dot(&b.cross(&c)) / 6.0
625 }
626}
627#[derive(Debug, Clone)]
631pub struct DistanceConstraint {
632 pub i: usize,
634 pub j: usize,
636 pub rest_length: Real,
638 pub compliance: Real,
640 pub(super) lambda: Real,
642}
643impl DistanceConstraint {
644 pub fn new(i: usize, j: usize, rest_length: Real, compliance: Real) -> Self {
646 Self {
647 i,
648 j,
649 rest_length,
650 compliance,
651 lambda: 0.0,
652 }
653 }
654 pub fn from_particles(
657 i: usize,
658 j: usize,
659 particles: &[SoftParticle],
660 compliance: Real,
661 ) -> Self {
662 let rest = (particles[i].position - particles[j].position).norm();
663 Self::new(i, j, rest, compliance)
664 }
665 pub fn reset_lambda(&mut self) {
668 self.lambda = 0.0;
669 }
670}
671#[derive(Debug, Clone)]
680pub struct IsometricBendingConstraint {
681 pub indices: [usize; 4],
683 pub stiffness: Real,
685 pub compliance: Real,
687 pub(super) q_matrix: [Real; 16],
689 pub(super) lambda: Real,
691}
692impl IsometricBendingConstraint {
693 pub fn from_particles(
695 indices: [usize; 4],
696 particles: &[SoftParticle],
697 stiffness: Real,
698 compliance: Real,
699 ) -> Self {
700 let p0 = particles[indices[0]].position;
701 let p1 = particles[indices[1]].position;
702 let p2 = particles[indices[2]].position;
703 let p3 = particles[indices[3]].position;
704 let q_matrix = Self::compute_q_matrix(&p0, &p1, &p2, &p3);
705 Self {
706 indices,
707 stiffness,
708 compliance,
709 q_matrix,
710 lambda: 0.0,
711 }
712 }
713 pub fn reset_lambda(&mut self) {
715 self.lambda = 0.0;
716 }
717 fn compute_q_matrix(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3) -> [Real; 16] {
724 let e = p1 - p0;
725 let e0_2 = p2 - p0;
726 let e1_2 = p2 - p1;
727 let e0_3 = p3 - p0;
728 let e1_3 = p3 - p1;
729 let cot = |a: &Vec3, b: &Vec3| -> Real {
730 let cross_norm = a.cross(b).norm();
731 if cross_norm < 1e-14 {
732 return 0.0;
733 }
734 a.dot(b) / cross_norm
735 };
736 let c01 = cot(&e0_2, &e1_2);
737 let c02 = cot(&e, &e0_2);
738 let c03 = cot(&(-e), &e1_2);
739 let c04 = cot(&e0_3, &e1_3);
740 let c05 = cot(&e, &e0_3);
741 let c06 = cot(&(-e), &e1_3);
742 let a1 = 0.5 * e0_2.cross(&e).norm();
743 let a2 = 0.5 * e0_3.cross(&e).norm();
744 let a_total = a1 + a2;
745 if a_total < 1e-14 {
746 return [0.0; 16];
747 }
748 let kk = [c03 + c06, c02 + c05, -c01, -c04];
749 let mut q = [0.0_f64; 16];
750 for row in 0..4 {
751 for col in 0..4 {
752 q[row * 4 + col] = kk[row] * kk[col] / (4.0 * a_total);
753 }
754 }
755 q
756 }
757}
758#[derive(Debug, Clone)]
763pub struct IsometricBendingConstraintV2 {
764 pub indices: [usize; 4],
766 pub stiffness: Real,
768 pub compliance: Real,
770 pub(super) q_matrix: [Real; 16],
772 pub(super) lambda: Real,
774}
775impl IsometricBendingConstraintV2 {
776 pub fn from_particles(
778 indices: [usize; 4],
779 particles: &[SoftParticle],
780 stiffness: Real,
781 compliance: Real,
782 ) -> Self {
783 let p = [
784 particles[indices[0]].position,
785 particles[indices[1]].position,
786 particles[indices[2]].position,
787 particles[indices[3]].position,
788 ];
789 let q_matrix = Self::build_q(&p[0], &p[1], &p[2], &p[3]);
790 Self {
791 indices,
792 stiffness,
793 compliance,
794 q_matrix,
795 lambda: 0.0,
796 }
797 }
798 pub fn reset_lambda(&mut self) {
800 self.lambda = 0.0;
801 }
802 pub fn bending_energy(&self, particles: &[SoftParticle]) -> Real {
804 let ps: Vec<Vec3> = self
805 .indices
806 .iter()
807 .map(|&i| particles[i].position)
808 .collect();
809 let mut energy = 0.0;
810 for axis in 0..3 {
811 let coords: [Real; 4] = std::array::from_fn(|k| match axis {
812 0 => ps[k].x,
813 1 => ps[k].y,
814 _ => ps[k].z,
815 });
816 for i in 0..4 {
817 for j in 0..4 {
818 energy += self.q_matrix[i * 4 + j] * coords[i] * coords[j];
819 }
820 }
821 }
822 0.5 * self.stiffness * energy
823 }
824 fn cot_angle(a: &Vec3, b: &Vec3) -> Real {
825 let cross = a.cross(b).norm();
826 if cross < 1e-14 {
827 return 0.0;
828 }
829 a.dot(b) / cross
830 }
831 fn build_q(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3) -> [Real; 16] {
832 let e0 = *p2 - *p0;
833 let e1 = *p2 - *p1;
834 let e2 = *p3 - *p0;
835 let e3 = *p3 - *p1;
836 let c01 = Self::cot_angle(&e0, &e1);
837 let c02 = Self::cot_angle(&e2, &e3);
838 let k = [c01 + c02, -(c01 + c02), c01, -c01 + c02 - c02, c02, -c02];
839 let coeff = [k[0], k[1], k[2], k[3]];
840 let mut q = [0.0f64; 16];
841 for i in 0..4 {
842 for j in 0..4 {
843 q[i * 4 + j] = coeff[i] * coeff[j];
844 }
845 }
846 q
847 }
848}
849#[derive(Debug, Clone)]
854pub struct BalloonPressureConstraint {
855 pub indices: [usize; 3],
857 pub pressure: Real,
859 pub rest_volume: Real,
861 pub compliance: Real,
863 pub(super) lambda: Real,
865}
866impl BalloonPressureConstraint {
867 pub fn new(indices: [usize; 3], pressure: Real, rest_volume: Real, compliance: Real) -> Self {
869 Self {
870 indices,
871 pressure,
872 rest_volume,
873 compliance,
874 lambda: 0.0,
875 }
876 }
877 pub fn reset_lambda(&mut self) {
879 self.lambda = 0.0;
880 }
881 pub fn triangle_volume_contribution(p0: &Vec3, p1: &Vec3, p2: &Vec3) -> Real {
884 p0.dot(&p1.cross(p2)) / 6.0
885 }
886 pub(super) fn triangle_normal_area(p0: &Vec3, p1: &Vec3, p2: &Vec3) -> Vec3 {
888 (p1 - p0).cross(&(p2 - p0))
889 }
890}
891#[derive(Debug, Clone)]
895pub struct BendingConstraint {
896 pub indices: [usize; 4],
898 pub rest_angle: Real,
900 pub compliance: Real,
902 pub(super) lambda: Real,
904}
905impl BendingConstraint {
906 pub fn new(indices: [usize; 4], rest_angle: Real, compliance: Real) -> Self {
908 Self {
909 indices,
910 rest_angle,
911 compliance,
912 lambda: 0.0,
913 }
914 }
915 pub fn from_particles(
918 indices: [usize; 4],
919 particles: &[SoftParticle],
920 compliance: Real,
921 ) -> Self {
922 let angle = Self::compute_dihedral(
923 &particles[indices[0]].position,
924 &particles[indices[1]].position,
925 &particles[indices[2]].position,
926 &particles[indices[3]].position,
927 );
928 Self::new(indices, angle, compliance)
929 }
930 pub fn reset_lambda(&mut self) {
932 self.lambda = 0.0;
933 }
934 pub(super) fn compute_dihedral(p0: &Vec3, p1: &Vec3, p2: &Vec3, p3: &Vec3) -> Real {
936 let e = p1 - p0;
937 let n1 = (p2 - p0).cross(&e);
938 let n2 = (p3 - p0).cross(&e);
939 let n1_len = n1.norm();
940 let n2_len = n2.norm();
941 if n1_len < 1e-12 || n2_len < 1e-12 {
942 return 0.0;
943 }
944 let n1 = n1 / n1_len;
945 let n2 = n2 / n2_len;
946 let cos_a = n1.dot(&n2).clamp(-1.0, 1.0);
947 cos_a.acos()
948 }
949}