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