1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//! Physics Bridge: ALICE-SDF → ALICE-Physics Integration
//!
//! Implements `alice_physics::SdfField` for `CompiledSdf`, enabling
//! SDF shapes to be used as colliders in the physics engine.
//!
//! # Usage
//!
//! ```ignore
//! use alice_sdf::prelude::*;
//! use alice_sdf::physics_bridge::CompiledSdfField;
//! use alice_physics::prelude::*;
//!
//! // Create SDF shape
//! let node = SdfNode::sphere(1.0)
//! .smooth_union(SdfNode::box3d(0.5, 2.0, 0.5), 0.2);
//! let compiled = CompiledSdf::compile(&node);
//!
//! // Wrap as physics collider
//! let field = CompiledSdfField::new(compiled);
//! let collider = SdfCollider::new_static(
//! Box::new(field),
//! Vec3Fix::ZERO,
//! QuatFix::IDENTITY,
//! );
//!
//! // Add to physics world
//! world.add_sdf_collider(collider);
//! ```
//!
//! # Performance
//!
//! - Single-point evaluation: `eval_compiled` (~10ns per query)
//! - Normal computation: `eval_compiled_normal` (tetrahedral, 4 evals)
//! - Both use the compiled bytecode VM — no tree traversal overhead
//!
//! Author: Moroya Sakamoto
use crate::compiled::{eval_compiled, eval_compiled_normal, CompiledSdf};
use alice_physics::SdfField;
use glam::Vec3;
use std::sync::Arc;
/// Wrapper around `CompiledSdf` that implements `alice_physics::SdfField`.
///
/// Thread-safe via `Arc<CompiledSdf>` — can be shared across physics threads.
pub struct CompiledSdfField {
sdf: Arc<CompiledSdf>,
/// Epsilon for gradient/normal computation (default: 0.001)
pub epsilon: f32,
}
impl CompiledSdfField {
/// Create a new physics-compatible SDF field from a compiled SDF.
pub fn new(sdf: CompiledSdf) -> Self {
Self {
sdf: Arc::new(sdf),
epsilon: 0.001,
}
}
/// Create from an existing `Arc<CompiledSdf>` (zero-cost sharing).
pub const fn from_arc(sdf: Arc<CompiledSdf>) -> Self {
Self {
sdf,
epsilon: 0.001,
}
}
/// Set the epsilon used for gradient computation.
pub const fn with_epsilon(mut self, epsilon: f32) -> Self {
self.epsilon = epsilon;
self
}
/// Get a reference to the underlying compiled SDF.
pub fn compiled(&self) -> &CompiledSdf {
&self.sdf
}
/// Get the Arc reference (for sharing across threads).
pub fn arc(&self) -> Arc<CompiledSdf> {
Arc::clone(&self.sdf)
}
}
// `CompiledSdfField` only holds an `Arc<CompiledSdf>` (Vec fields, no
// interior mutability), so `Send + Sync` are derived automatically — the
// `unsafe impl`s that used to assert this were redundant.
impl SdfField for CompiledSdfField {
#[inline]
fn distance(&self, x: f32, y: f32, z: f32) -> f32 {
eval_compiled(&self.sdf, Vec3::new(x, y, z))
}
#[inline]
fn normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
let n = eval_compiled_normal(&self.sdf, Vec3::new(x, y, z), self.epsilon);
(n.x, n.y, n.z)
}
// `distance_and_normal` is deliberately NOT overridden.
//
// The trait's default is `(self.distance(x, y, z), self.normal(x, y, z))`
// — an exact distance (1 evaluation) plus the tetrahedral normal (4), so 5
// in total. This crate used to override it with
// `eval_compiled_distance_and_normal`, which reuses the normal's four
// offset samples and takes their *average* as the distance. That is 4
// evaluations instead of 5, and the helper's own doc is explicit that the
// centre distance is approximated (error O(epsilon^2)).
//
// The override therefore did not compute the same query more efficiently —
// it answered a different question. Measured on 2026-09-30 over a
// sphere-union-box field at 24 points, the override's distance was larger
// than `distance()` at every point, by 4.768e-7 to 7.451e-7: a one-sided
// bias, not rounding.
//
// That mattered because the trait's own contract is set by its default
// ("default: two separate calls"), and callers take it at face value:
// `alice_physics::sdf_adaptive::AdaptiveSdfEvaluator::evaluate_and_cache`
// (1.4.0) stores `distance_and_normal`'s distance in its cache, while
// `collide_point_sdf` derives contact depth from `distance`. Two paths in
// the same crate disagreed about the distance at the same point.
//
// Dropping the override makes them agree by construction: there is no
// second distance to drift. The cost is the fifth evaluation, which is what
// an exact distance costs. Oracle:
// `tests/test_physics_bridge_determinism.rs::distance_and_normal_returns_the_exact_distance`.
//
// `eval_compiled_distance_and_normal` itself is kept — it is a documented
// 4-evaluation approximation and callers that want that trade-off can ask
// for it by name.
}
/// Convenience function: compile an SDF node and wrap as a physics field.
pub fn sdf_to_physics_field(node: &crate::types::SdfNode) -> CompiledSdfField {
let compiled = CompiledSdf::compile(node);
CompiledSdfField::new(compiled)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::SdfNode;
use alice_physics::sdf_collider::{collide_point_sdf, SdfCollider};
use alice_physics::{Fix128, QuatFix, Vec3Fix};
#[test]
fn test_compiled_sdf_as_physics_field() {
let node = SdfNode::sphere(1.0);
let field = sdf_to_physics_field(&node);
// Distance at origin should be -1.0 (inside unit sphere)
let d = field.distance(0.0, 0.0, 0.0);
assert!((d - (-1.0)).abs() < 0.01, "d={}", d);
// Distance at (2, 0, 0) should be +1.0 (outside)
let d2 = field.distance(2.0, 0.0, 0.0);
assert!((d2 - 1.0).abs() < 0.01, "d2={}", d2);
// Normal at (1, 0, 0) should be (1, 0, 0)
let (nx, ny, nz) = field.normal(1.0, 0.0, 0.0);
assert!(nx > 0.99, "nx={}", nx);
assert!(ny.abs() < 0.01, "ny={}", ny);
assert!(nz.abs() < 0.01, "nz={}", nz);
}
#[test]
fn test_sphere_collider_via_bridge() {
// Create a complex SDF: sphere with box subtracted
let node = SdfNode::sphere(2.0);
let field = sdf_to_physics_field(&node);
let collider = SdfCollider::new_static(Box::new(field), Vec3Fix::ZERO, QuatFix::IDENTITY);
// Point inside sphere — should collide
let point_inside = Vec3Fix::from_f32(0.5, 0.0, 0.0);
let result = collide_point_sdf(point_inside, &collider);
assert!(result.is_some(), "Point inside should collide");
// Point far outside sphere — should not collide
let point_outside = Vec3Fix::from_f32(5.0, 0.0, 0.0);
let result2 = collide_point_sdf(point_outside, &collider);
assert!(result2.is_none(), "Point outside should not collide");
}
#[test]
fn test_csg_collider_via_bridge() {
// CSG: sphere with box hole
let node = SdfNode::sphere(2.0).subtract(SdfNode::box3d(0.5, 0.5, 0.5));
let field = sdf_to_physics_field(&node);
let collider = SdfCollider::new_static(Box::new(field), Vec3Fix::ZERO, QuatFix::IDENTITY);
// Origin is inside the subtracted box — should be outside the CSG shape
let origin = Vec3Fix::ZERO;
let result = collide_point_sdf(origin, &collider);
assert!(
result.is_none(),
"Origin inside box hole should not collide"
);
// Point on sphere shell (1.5, 0, 0) — inside the SDF
let shell_point = Vec3Fix::from_f32(1.5, 0.0, 0.0);
let result2 = collide_point_sdf(shell_point, &collider);
assert!(
result2.is_some(),
"Point inside sphere shell should collide"
);
}
#[test]
fn test_physics_world_with_sdf() {
use alice_physics::{PhysicsConfig, PhysicsWorld, RigidBody};
let node = SdfNode::Plane {
normal: glam::Vec3::Y,
distance: 0.0,
};
let field = sdf_to_physics_field(&node);
let config = PhysicsConfig::default();
let mut world = PhysicsWorld::new(config);
world.set_sdf_collision_radius(Fix128::from_f32(0.5));
// Add falling body
let body = RigidBody::new_dynamic(Vec3Fix::from_f32(0.0, 5.0, 0.0), Fix128::ONE);
world.add_body(body);
// Add SDF ground
world.add_sdf_collider(SdfCollider::new_static(
Box::new(field),
Vec3Fix::ZERO,
QuatFix::IDENTITY,
));
// Simulate
let dt = Fix128::from_f32(1.0 / 60.0);
for _ in 0..120 {
world.step(dt);
}
// Body should be near ground
let y = world.bodies[0].position.y.to_f32();
assert!(y < 5.0, "Body should have fallen");
assert!(y > -1.0, "Body should not have fallen through, y={}", y);
}
}