Skip to main content

alice_sdf/
lib.rs

1//! # ALICE-SDF
2//!
3//! Signed distance functions for Rust. A shape is a tree of primitives, CSG
4//! operations, transforms and modifiers ([`SdfNode`]). The crate
5//! evaluates the tree on the CPU (scalar, SIMD, BVH and JIT evaluators),
6//! transpiles it to GLSL, WGSL, HLSL and Metal, and turns it into meshes and file
7//! formats.
8//!
9//! The CPU evaluators return bit-identical distances for the same tree and point
10//! on every platform CI tests: every transcendental goes through
11//! [`alice-det-math`](https://crates.io/crates/alice-det-math) and `a * b + c` is
12//! never fused. The GPU shaders agree with the CPU within a tolerance.
13//!
14//! ## Example
15//!
16//! ```rust
17//! use alice_sdf::prelude::*;
18//!
19//! // A unit sphere minus a box (box3d takes full extents)
20//! let shape = SdfNode::sphere(1.0).subtract(SdfNode::box3d(1.0, 1.0, 1.0));
21//!
22//! // Signed distance: negative inside, zero on the surface, positive outside
23//! let p = Vec3::new(0.9, 0.0, 0.0);
24//! let d = eval(&shape, p);
25//! assert!(d < 0.0);
26//!
27//! // Compiled to bytecode for repeated evaluation, with the same bits
28//! let compiled = CompiledSdf::compile(&shape);
29//! assert_eq!(eval_compiled(&compiled, p).to_bits(), d.to_bits());
30//!
31//! // A triangle mesh by marching cubes
32//! let mesh = sdf_to_mesh(
33//!     &shape,
34//!     Vec3::splat(-1.5),
35//!     Vec3::splat(1.5),
36//!     &MarchingCubesConfig::default(),
37//! );
38//! assert!(!mesh.indices.is_empty());
39//! ```
40//!
41//! ## Cargo features
42//!
43//! The `physics`, `codec` and `sdf-cache` features link crates licensed
44//! `AGPL-3.0-or-later`; the default features and all others do not.
45//!
46//! <!-- readme-sync: features -->
47//! | Feature | Default | Description |
48//! |---------|:-------:|-------------|
49//! | `cli` | yes | The `alice-sdf` command-line tool (clap). |
50//! | `image` | | PNG / JPEG decoding for heightmaps and texture fitting. |
51//! | `texture-fit` | | Fit a bitmap texture with procedural noise formulas. Implies `image` and `cli`. |
52//! | `jit` | | Native evaluation through Cranelift (scalar and 8-wide). |
53//! | `gpu` | | GPU evaluation and WGSL output through wgpu. |
54//! | `gpu-mesh` | | Marching cubes on the GPU. Implies `gpu`. |
55//! | `volume` | | Bake a field into a 3D texture. Implies `gpu`. |
56//! | `glsl` | | GLSL output (Unity, OpenGL, Vulkan, Shadertoy). |
57//! | `hlsl` | | HLSL output (Unreal Engine, DirectX). |
58//! | `msl` | | Metal Shading Language output, converted from WGSL with naga. Implies `gpu`. |
59//! | `blinkscript` | | BlinkScript output for Nuke. Implies `hlsl`. |
60//! | `rust` | | Dependency-free Rust source (`fn sdf`, `fn normal`) bit-identical to `eval_compiled`, for `build.rs`. |
61//! | `all-shaders` | | `gpu`, `glsl`, `hlsl`, `msl` and `blinkscript`. |
62//! | `svo` | | Sparse voxel octree. |
63//! | `svo-gpu` | | Sparse voxel octree with GPU support. Implies `svo` and `gpu`. |
64//! | `destruction` | | Voxel destruction. |
65//! | `terrain` | | Heightmap terrain with erosion and caves. |
66//! | `gi` | | Cone-traced global illumination. Implies `svo`. |
67//! | `aaa` | | `volume`, `gpu-mesh`, `svo-gpu`, `destruction`, `terrain` and `gi`. |
68//! | `ffi` | | C ABI for C, C++, C#, Unity and Unreal Engine. |
69//! | `unity` | | `ffi` and `glsl`. |
70//! | `unreal` | | `ffi`, `hlsl`, `glsl` and `gpu`: what the Unreal Engine plugin calls. |
71//! | `python` | | Python bindings (PyO3 + NumPy). |
72//! | `godot` | | Godot 4 GDExtension. |
73//! | `wasm` | | WebAssembly bindings through `wasm-bindgen` (built for `wasm32` only). |
74//! | `openvdb` | | OpenVDB float grid input and output. |
75//! | `physics` | | **AGPL.** SDF colliders and simulation modifiers for [`alice-physics`](https://crates.io/crates/alice-physics). |
76//! | `codec` | | **AGPL.** Volume compression with [`alice-codec`](https://crates.io/crates/alice-codec). |
77//! | `sdf-cache` | | **AGPL.** Evaluation cache with [`alice-cache`](https://crates.io/crates/alice-cache). |
78//! | `asp` | | ALICE Streaming Protocol packets with [`libasp`](https://crates.io/crates/libasp) (its default features only). |
79//! | `font` | | Glyph outlines from `alice-font`. Inert on crates.io: also needs `--cfg alice_font_bridge` and a local `alice-font`. |
80//!
81//! Module overview: <https://github.com/ext-sakamoro/ALICE-SDF/blob/main/docs/MODULES.md>
82
83#![warn(missing_docs)]
84// The clippy policy (pedantic + nursery with documented exceptions) lives in
85// Cargo.toml [lints.clippy] so tests, benches and examples share it.
86
87pub mod animation;
88pub mod cache;
89pub mod collision;
90pub mod compiled;
91mod crispy;
92pub mod eval;
93pub mod fidelity;
94pub mod interval;
95pub mod io;
96pub mod llm_schema;
97pub mod material;
98pub mod mesh;
99pub mod modifiers;
100pub mod morphology;
101pub mod neural;
102pub mod npr;
103pub mod operations;
104pub mod optimize;
105pub mod primitives;
106mod raw_minmax;
107pub mod raycast;
108pub mod soa;
109pub mod tight_aabb;
110pub mod transforms;
111pub mod types;
112
113// New modules: analysis, 2D, and bridges
114pub mod autodiff;
115pub mod constraint;
116pub mod diff;
117pub mod heatmap;
118pub mod incremental;
119pub mod measure;
120pub mod sdf2d;
121pub mod shell;
122pub mod validity;
123
124// `font` alone is inert on crates.io (no `alice-font` dep declared);
125// the bridge needs `--cfg alice_font_bridge` + a local path dep.
126#[cfg(all(feature = "font", alice_font_bridge))]
127pub mod font_bridge;
128
129#[cfg(feature = "python")]
130pub mod python;
131
132#[cfg(feature = "godot")]
133pub mod godot;
134
135#[cfg(feature = "ffi")]
136pub mod ffi;
137
138#[cfg(feature = "codec")]
139pub mod codec_bridge;
140
141#[cfg(feature = "physics")]
142pub mod physics_bridge;
143
144#[cfg(feature = "physics")]
145pub mod sim_bridge;
146
147#[cfg(feature = "physics")]
148pub mod live_sdf;
149
150#[cfg(feature = "asp")]
151pub mod asp_bridge;
152
153#[cfg(feature = "sdf-cache")]
154pub mod cache_bridge;
155
156#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
157pub mod wasm;
158
159#[cfg(feature = "texture-fit")]
160pub mod texture;
161
162#[cfg(feature = "volume")]
163pub mod volume;
164
165#[cfg(feature = "svo")]
166pub mod svo;
167
168#[cfg(feature = "destruction")]
169pub mod destruction;
170
171#[cfg(feature = "terrain")]
172pub mod terrain;
173
174#[cfg(feature = "gi")]
175pub mod gi;
176
177/// Library version
178pub const VERSION: &str = env!("CARGO_PKG_VERSION");
179
180/// The numeric semantics the CPU evaluators compute distances with
181///
182/// Re-exported from `alice-det-math`, which derives it from the bit pins of its
183/// kernels: it changes exactly when one of them would return different bits for
184/// the same input. A caller that stores or identifies distances evaluated by
185/// this crate (a baked field, a recorded collision trace, a content hash of a
186/// tree plus its samples) can mix it in, so a change of arithmetic shows up as
187/// a different identifier instead of a silent mismatch. `tests/test_det_golden.rs`
188/// pins its value, so moving to another `alice-det-math` is visible here too.
189pub use alice_det_math::SEMANTICS_ID;
190
191/// Prelude - commonly used types and functions
192pub mod prelude {
193    pub use crate::animation::{
194        morph, AnimatedSdf, Interpolation, Keyframe, LoopMode, Timeline, Track,
195    };
196    pub use crate::autodiff::{
197        eval_hessian, eval_with_gradient, gaussian_curvature, mean_curvature, principal_curvatures,
198        Dual, Dual3,
199    };
200    #[cfg(feature = "codec")]
201    pub use crate::codec_bridge::{
202        compress_sdf, compression_ratio, decode_sdf_volume, decompress_sdf, encode_sdf_volume,
203        volume_stats, voxelize_sdf, voxelize_sdf_uniform, CompressResult, EncodeConfig, SdfVolume,
204        VolumeStats,
205    };
206    pub use crate::collision::{
207        compute_manifold, sdf_ccd, sdf_closest_point, sdf_collide, sdf_distance, sdf_overlap,
208        ContactManifold, SdfContact,
209    };
210    pub use crate::compiled::{
211        eval_compiled, eval_compiled_batch_simd, eval_compiled_batch_simd_parallel,
212        eval_compiled_batch_soa, eval_compiled_batch_soa_parallel, eval_compiled_bvh,
213        eval_compiled_distance_and_normal, eval_compiled_normal, eval_compiled_simd,
214        get_scene_aabb, AabbPacked, CompileError, CompiledSdf, CompiledSdfBvh, RefitError, Vec3x8,
215    };
216    pub use crate::constraint::{
217        Constraint, ConstraintKind, ConstraintSolver, ParamId, SolveResult,
218    };
219    #[cfg(feature = "destruction")]
220    pub use crate::destruction::{
221        carve, carve_batch, generate_debris, voronoi_fracture, CarveShape, ChunkMesh, DebrisConfig,
222        DebrisPiece, DestructionResult, FractureConfig, FracturePiece, MutableVoxelGrid,
223    };
224    pub use crate::diff::{
225        apply_patch, invert_patch, merge_patches, tree_diff, tree_hash, DiffError, DiffOp,
226        TreePatch,
227    };
228    pub use crate::eval::{
229        eval, eval_batch, eval_batch_parallel, eval_gradient, eval_grid, eval_material,
230        eval_normal, gradient, normal,
231    };
232    #[cfg(feature = "gi")]
233    pub use crate::gi::{
234        bake_irradiance_grid, cone_trace, direct_lighting, sky_color, trace_hemisphere,
235        BakeGiConfig, ConeTraceConfig, ConeTraceResult, DirectionalLight, IrradianceGrid,
236        IrradianceProbe, PointLight,
237    };
238    pub use crate::heatmap::{
239        generate_heatmap, heatmap_to_rgba, ColorMap, Heatmap, HeatmapConfig, SlicePlane,
240    };
241    pub use crate::incremental::{IncrementalError, InstructionSlot, ParamDependencyIndex};
242    pub use crate::io::{
243        export_alembic, export_fbx, export_glb, export_glb_bytes, export_gltf_json, export_nanite,
244        export_nanite_json, export_nanite_with_config, export_obj, export_usda, get_info,
245        import_fbx, import_glb, import_glb_bytes, import_obj, load, load_asdf, load_asdf_json,
246        save, save_asdf, save_asdf_json, AlembicConfig, FbxConfig, FbxFormat, FbxUpAxis,
247        GltfConfig, NaniteExportConfig, ObjConfig, UsdConfig, UsdUpAxis,
248    };
249    #[cfg(feature = "physics")]
250    pub use crate::live_sdf::{LiveMesh, LiveMeshConfig, LiveSdf};
251    pub use crate::material::{
252        material_lerp, Material, MaterialLibrary, StandardMaterials, TextureSlot,
253    };
254    pub use crate::measure::{
255        estimate_center_of_mass, estimate_surface_area, estimate_volume, AreaEstimate,
256        CenterOfMass, VolumeEstimate,
257    };
258    #[allow(deprecated)] // re-export kept until the next major
259    pub use crate::mesh::point_cloud_to_sdf;
260    pub use crate::mesh::{
261        // Adaptive marching cubes
262        adaptive_marching_cubes,
263        apply_uvs,
264        compute_aabb,
265        compute_acmr,
266        compute_bounding_sphere,
267        compute_convex_hull,
268        compute_quality,
269        convex_decomposition,
270        // Mesh decimation
271        decimate,
272        deduplicate_vertices,
273        detect_primitive,
274        // Dual Contouring
275        dual_contouring,
276        dual_contouring_compiled,
277        extract_hermite,
278        // Lightmap UV generation
279        generate_lightmap_uvs,
280        generate_lightmap_uvs_fast,
281        generate_lod_chain,
282        generate_lod_chain_decimated,
283        generate_nanite_mesh,
284        marching_cubes,
285        mesh_to_sdf,
286        mesh_to_sdf_exact,
287        // Vertex cache optimization
288        optimize_vertex_cache,
289        primitives_to_csg,
290        sdf_to_mesh,
291        simplify_collision,
292        // UV unwrapping (LSCM)
293        uv_unwrap,
294        validate_mesh,
295        AdaptiveConfig,
296        BoundingSphere,
297        // Physics collision primitives
298        CollisionAabb,
299        CollisionMesh,
300        ConvexDecomposition,
301        ConvexHull,
302        DecimateConfig,
303        DecimationLodConfig,
304        DualContouringConfig,
305        // Primitive fitting
306        FittedPrimitive,
307        FittingConfig,
308        HermiteConfig,
309        // Hermite data extraction
310        HermitePoint,
311        // LOD generation
312        LodChain,
313        LodConfig,
314        LodSelector,
315        MarchingCubesConfig,
316        Mesh,
317        MeshBvh,
318        MeshQuality,
319        MeshRepair,
320        MeshSdf,
321        MeshSignMode,
322        MeshToSdfConfig,
323        MeshToSdfStrategy,
324        // Manifold mesh validation & repair
325        MeshValidation,
326        // Nanite clusters
327        NaniteCluster,
328        NaniteConfig,
329        NaniteMesh,
330        // Point cloud SDF
331        PointCloudSdf,
332        PointCloudSdfConfig,
333        Triangle,
334        UvChart,
335        UvUnwrapConfig,
336        UvUnwrapResult,
337        Vertex,
338        // Convex decomposition (V-HACD)
339        VhacdConfig,
340    };
341    #[cfg(feature = "gpu")]
342    pub use crate::mesh::{
343        gpu_marching_cubes, gpu_marching_cubes_from_shader, GpuMarchingCubesConfig,
344    };
345    pub use crate::modifiers::*;
346    pub use crate::npr::compiled_color::{
347        ColorOp, CompiledColorPipeline, NprBatchContext8, NprColorBatch8,
348    };
349    pub use crate::npr::composition::{bloom_toon, chromatic_offsets, vignette};
350    pub use crate::npr::distortion::{hand_drawn_jitter, line_boil, sketch_wobble};
351    pub use crate::npr::dsl::{NprColorContext, NprColorNode, PaletteSource};
352    pub use crate::npr::dsl_shader::{
353        transpile_npr_color_node, NprShaderContext, NprShaderSnippet,
354    };
355    pub use crate::npr::hatch::{cross_hatch, hatch_lines, paper_grain, pencil_shade};
356    pub use crate::npr::motion::{impact_flash, speed_line};
357    pub use crate::npr::noise::{
358        fbm, HashNoise, NoiseField, PerlinNoise, SimplexNoise, WorleyNoise,
359    };
360    pub use crate::npr::outline::{
361        composite_outline, curvature_outline, depth_step_outline, distance_field_outline,
362        distance_field_outline_soft,
363    };
364    pub use crate::npr::palette::{palette_gradient, season_palette, time_of_day};
365    pub use crate::npr::rim::{fresnel_rim, procedural_matcap, stylized_specular};
366    #[cfg(any(feature = "glsl", feature = "hlsl", feature = "gpu"))]
367    pub use crate::npr::scene_composer::SceneShaderBuilder;
368    pub use crate::npr::sdf_integration::{
369        curvature_outline_from_node, distance_outline_from_node, soft_toon_shade_from_node,
370        toon_shade_from_node,
371    };
372    pub use crate::npr::shader_glue::{
373        full_helpers_for, helpers_for, palette_helpers_for, ShaderLanguage, NPR_GLSL_HELPERS,
374        NPR_GLSL_PALETTE_HELPERS, NPR_HLSL_HELPERS, NPR_HLSL_PALETTE_HELPERS, NPR_WGSL_HELPERS,
375        NPR_WGSL_PALETTE_HELPERS,
376    };
377    pub use crate::npr::sky::{
378        distance_color_quantize, light_shaft_beam, puffy_cloud_layer, sky_gradient_bands, sun_disc,
379    };
380    pub use crate::npr::toon::{posterize_color, soft_toon_ramp, toon_ramp, two_tone};
381    pub use crate::npr::{NprColor, NprInput};
382    pub use crate::operations::*;
383    pub use crate::primitives::*;
384    pub use crate::raycast::{
385        ambient_occlusion, raycast, raycast_batch, raymarch, raymarch_with_config, soft_shadow,
386        RaymarchConfig, RaymarchResult,
387    };
388    pub use crate::sdf2d::{eval_2d, eval_2d_batch, eval_2d_normal, Sdf2dNode};
389    pub use crate::shell::{
390        eval_shell, eval_shell_batch, eval_shell_batch_parallel, eval_shell_compiled,
391        eval_shell_compiled_batch_parallel, eval_shell_gradient, shell_node, ShellConfig,
392    };
393    #[cfg(feature = "physics")]
394    pub use crate::sim_bridge::{attach_physics, simulate_sdf, SimulatedSdf};
395    #[cfg(all(feature = "physics", feature = "gpu"))]
396    pub use crate::sim_bridge::{gpu_mesh_with_physics, GpuPhysicsBundle};
397    pub use crate::soa::{SoADistances, SoAPoints};
398    #[cfg(feature = "svo")]
399    pub use crate::svo::{
400        build_svo, build_svo_compiled, linearize_svo, svo_nearest_surface, svo_query_point,
401        svo_ray_query, LinearizedSvo, SparseVoxelOctree, SvoBuildConfig, SvoChunk, SvoNode,
402        SvoRayHit, SvoStreamingCache,
403    };
404    #[cfg(all(feature = "terrain", feature = "image"))]
405    pub use crate::terrain::HeightmapImageConfig;
406    #[cfg(feature = "terrain")]
407    pub use crate::terrain::{
408        erode, generate_cave_sdf, terrain_sdf, CaveConfig, ClipmapLevel, ClipmapMesh,
409        ClipmapTerrain, ErosionConfig, Heightmap, SplatLayer, Splatmap, TerrainConfig,
410    };
411    pub use crate::transforms::*;
412    pub use crate::types::{Aabb, Hit, Ray, SdfCategory, SdfMetadata, SdfNode, SdfTree};
413    #[cfg(feature = "volume")]
414    pub use crate::volume::{
415        bake_volume, bake_volume_with_normals, export_dds_3d, export_raw, generate_mip_chain,
416        BakeChannels, BakeConfig, Volume3D, VoxelDistGrad,
417    };
418    pub use glam::{Quat, Vec3};
419}
420
421// Re-exports for convenience
422pub use cache::{
423    compute_cache_key, hash_sdf_node, CacheConfig, ChunkCoord, ChunkedCacheConfig,
424    ChunkedMeshCache, MeshCache, MeshCacheKey,
425};
426pub use eval::eval;
427pub use io::{load, save};
428pub use mesh::sdf_to_mesh;
429pub use types::{SdfNode, SdfTree};
430
431/// Items removed or made crate-private in 5.0.0 stay out of the public API.
432/// Each block below must fail to compile (`cargo test --doc` runs them):
433///
434/// ```compile_fail
435/// use alice_sdf::crispy::fnv1a_hash;
436/// ```
437///
438/// ```compile_fail
439/// let _ = alice_sdf::compiled::Vec3x8::zero();
440/// ```
441///
442/// ```compile_fail
443/// let _ = alice_sdf::primitives::eval_primitive_unchecked;
444/// ```
445///
446/// ```compile_fail
447/// use alice_sdf::mesh::bvh::Triangle;
448/// ```
449///
450/// ```compile_fail
451/// use alice_sdf::mesh::primitive_fitting::PrimitiveType;
452/// ```
453///
454/// ```compile_fail
455/// use alice_sdf::primitives::PrimitiveType;
456/// ```
457///
458/// ```compile_fail
459/// let _ = alice_sdf::primitives::eval_primitive;
460/// ```
461///
462/// The replacements compile, and `mesh::Aabb` is `types::Aabb`:
463///
464/// ```
465/// use alice_sdf::mesh::{BvhTriangle, FittedPrimitiveKind};
466/// let a: alice_sdf::types::Aabb = alice_sdf::mesh::Aabb::empty();
467/// let _ = (a, FittedPrimitiveKind::Sphere, std::mem::size_of::<BvhTriangle>());
468/// let _ = alice_sdf::compiled::Vec3x8::splat(glam::Vec3::ONE);
469/// let _ = alice_sdf::primitives::sdf_sphere;
470/// ```
471#[cfg(doctest)]
472struct RemovedIn5;
473
474#[cfg(test)]
475mod tests {
476    use super::prelude::*;
477
478    #[test]
479    fn test_basic_workflow() {
480        // Create a shape: sphere with hole carved out
481        let shape = SdfNode::sphere(1.0).subtract(SdfNode::box3d(0.5, 0.5, 0.5));
482
483        // Evaluate at origin - box is inside sphere, so origin is carved out
484        let d = eval(&shape, Vec3::ZERO);
485        assert!(d > 0.0); // Outside (carved by box)
486
487        // Evaluate at surface - should be near zero
488        let d_surface = eval(&shape, Vec3::new(1.0, 0.0, 0.0));
489        assert!(d_surface.abs() < 0.01);
490
491        // Create tree
492        let tree = SdfTree::new(shape);
493        assert!(tree.node_count() > 0);
494    }
495
496    #[test]
497    fn test_complex_tree() {
498        let shape = SdfNode::sphere(1.0)
499            .smooth_union(SdfNode::cylinder(0.5, 2.0), 0.2)
500            .subtract(SdfNode::box3d(0.5, 0.5, 0.5).repeat_infinite(1.0, 1.0, 1.0))
501            .twist(0.5)
502            .scale(2.0);
503
504        assert!(shape.node_count() >= 6);
505    }
506
507    #[test]
508    fn test_mesh_generation() {
509        let sphere = SdfNode::sphere(1.0);
510        let config = MarchingCubesConfig {
511            resolution: 8,
512            iso_level: 0.0,
513            compute_normals: true,
514            ..Default::default()
515        };
516
517        let mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
518        assert!(mesh.vertex_count() > 0);
519        assert!(mesh.triangle_count() > 0);
520    }
521
522    #[test]
523    fn test_raymarching() {
524        let sphere = SdfNode::sphere(1.0);
525        let ray = Ray::new(Vec3::new(-5.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0));
526
527        let hit = raycast(&sphere, ray, 10.0);
528        assert!(hit.is_some());
529
530        let hit = hit.unwrap();
531        assert!((hit.distance - 4.0).abs() < 0.1);
532    }
533
534    #[test]
535    fn test_production_stress_high_res() {
536        // High-resolution mesh generation (production scale)
537        let shape = SdfNode::sphere(1.0).smooth_union(SdfNode::box3d(0.5, 0.5, 0.5), 0.2);
538        let config = MarchingCubesConfig {
539            resolution: 64,
540            iso_level: 0.0,
541            compute_normals: true,
542            ..Default::default()
543        };
544        let mesh = sdf_to_mesh(&shape, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
545
546        // Should produce a substantial mesh
547        assert!(
548            mesh.vertex_count() > 1000,
549            "Expected >1000 vertices, got {}",
550            mesh.vertex_count()
551        );
552        assert!(
553            mesh.triangle_count() > 500,
554            "Expected >500 triangles, got {}",
555            mesh.triangle_count()
556        );
557    }
558
559    #[test]
560    fn test_production_multi_material_pipeline() {
561        // Full pipeline: SDF → mesh → optimize → glTF with multi-material
562        let shape = SdfNode::sphere(1.0);
563        let config = MarchingCubesConfig {
564            resolution: 16,
565            iso_level: 0.0,
566            compute_normals: true,
567            ..Default::default()
568        };
569        let mut mesh = sdf_to_mesh(&shape, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
570
571        // Assign materials to half the triangles
572        let half = mesh.vertices.len() / 2;
573        for v in mesh.vertices[half..].iter_mut() {
574            v.material_id = 1;
575        }
576
577        // Optimize vertex cache
578        let acmr_before = compute_acmr(&mesh, 32);
579        optimize_vertex_cache(&mut mesh);
580        let acmr_after = compute_acmr(&mesh, 32);
581        assert!(acmr_after <= acmr_before + 0.01);
582
583        // Create materials
584        let mut mat_lib = MaterialLibrary::new();
585        mat_lib.add(
586            Material::metal("Chrome", 0.9, 0.9, 0.9, 0.2)
587                .with_albedo_map("textures/chrome_albedo.png")
588                .with_normal_map("textures/chrome_normal.png"),
589        );
590
591        // Export glTF with multi-material
592        let path = std::env::temp_dir().join("alice_stress_multi_mat.glb");
593        export_glb(&mesh, &path, &GltfConfig::aaa(), Some(&mat_lib)).unwrap();
594
595        let bytes = std::fs::read(&path).unwrap();
596        assert!(bytes.len() > 100);
597        // Verify it's valid GLB
598        assert_eq!(&bytes[0..4], &0x46546C67u32.to_le_bytes());
599
600        std::fs::remove_file(&path).ok();
601    }
602
603    #[test]
604    fn test_production_collision_pipeline() {
605        // Full pipeline: SDF → mesh → collision primitives
606        let shape = SdfNode::sphere(1.0).subtract(SdfNode::box3d(0.5, 0.5, 0.5));
607        let config = MarchingCubesConfig {
608            resolution: 16,
609            iso_level: 0.0,
610            compute_normals: true,
611            ..Default::default()
612        };
613        let mesh = sdf_to_mesh(&shape, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
614
615        // AABB
616        let aabb = compute_aabb(&mesh);
617        assert!(aabb.contains(Vec3::new(0.5, 0.0, 0.0)));
618
619        // Bounding sphere
620        let bs = compute_bounding_sphere(&mesh);
621        assert!(bs.radius > 0.5);
622
623        // Convex hull
624        let hull = compute_convex_hull(&mesh);
625        assert!(hull.vertices.len() >= 4);
626        assert!(hull.indices.len() >= 12);
627
628        // Simplified collision
629        let simplified = simplify_collision(&mesh, 8);
630        assert!(simplified.vertices.len() < mesh.vertices.len());
631    }
632
633    #[test]
634    fn test_compiled_vs_interpreted() {
635        // Complex shape to test
636        let shape = SdfNode::sphere(1.0)
637            .smooth_union(
638                SdfNode::cylinder(0.3, 1.5).rotate_euler(1.57, 0.0, 0.0),
639                0.2,
640            )
641            .subtract(SdfNode::box3d(0.4, 0.4, 0.4))
642            .translate(0.5, 0.0, 0.0);
643
644        let compiled = CompiledSdf::compile(&shape);
645
646        // Test at many points
647        let test_points = [
648            Vec3::ZERO,
649            Vec3::new(1.0, 0.0, 0.0),
650            Vec3::new(0.5, 0.5, 0.5),
651            Vec3::new(-1.0, -1.0, -1.0),
652            Vec3::new(0.25, 0.25, 0.25),
653            Vec3::new(2.0, 0.0, 0.0),
654        ];
655
656        for p in test_points {
657            let d_interp = eval(&shape, p);
658            let d_compiled = eval_compiled(&compiled, p);
659            assert!(
660                (d_interp - d_compiled).abs() < 0.001,
661                "Mismatch at {:?}: interp={}, compiled={}",
662                p,
663                d_interp,
664                d_compiled
665            );
666        }
667    }
668}