Skip to main content

oxihuman_export/
nff_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Neutral File Format (NFF) export.
6//! NFF is a simple scene description language used by Eric Haines' ray tracers.
7
8/// An NFF point light source.
9#[derive(Clone, Debug)]
10pub struct NffLight {
11    pub position: [f32; 3],
12    pub color: [f32; 3],
13}
14
15/// NFF material surface properties.
16#[derive(Clone, Debug)]
17pub struct NffSurface {
18    pub color: [f32; 3],
19    pub kd: f32,
20    pub ks: f32,
21    pub shine: f32,
22    pub transmittance: f32,
23    pub ior: f32,
24}
25
26impl Default for NffSurface {
27    fn default() -> Self {
28        Self {
29            color: [0.8, 0.8, 0.8],
30            kd: 1.0,
31            ks: 0.0,
32            shine: 0.0,
33            transmittance: 0.0,
34            ior: 1.0,
35        }
36    }
37}
38
39/// An NFF polygon (flat, convex).
40#[derive(Clone, Debug)]
41pub struct NffPolygon {
42    pub vertices: Vec<[f32; 3]>,
43    pub surface: NffSurface,
44}
45
46/// An NFF sphere primitive.
47#[derive(Clone, Debug)]
48pub struct NffSphere {
49    pub center: [f32; 3],
50    pub radius: f32,
51    pub surface: NffSurface,
52}
53
54/// An NFF scene document.
55#[derive(Clone, Debug, Default)]
56pub struct NffDocument {
57    pub background: [f32; 3],
58    pub lights: Vec<NffLight>,
59    pub polygons: Vec<NffPolygon>,
60    pub spheres: Vec<NffSphere>,
61}
62
63/// Create a new NFF document with a white background.
64pub fn new_nff_document() -> NffDocument {
65    NffDocument {
66        background: [1.0, 1.0, 1.0],
67        ..Default::default()
68    }
69}
70
71/// Set the background color.
72pub fn nff_set_background(doc: &mut NffDocument, color: [f32; 3]) {
73    doc.background = color;
74}
75
76/// Add a point light.
77pub fn nff_add_light(doc: &mut NffDocument, position: [f32; 3], color: [f32; 3]) {
78    doc.lights.push(NffLight { position, color });
79}
80
81/// Add a polygon.
82pub fn nff_add_polygon(doc: &mut NffDocument, vertices: Vec<[f32; 3]>, surface: NffSurface) {
83    doc.polygons.push(NffPolygon { vertices, surface });
84}
85
86/// Add a sphere.
87pub fn nff_add_sphere(doc: &mut NffDocument, center: [f32; 3], radius: f32, surface: NffSurface) {
88    doc.spheres.push(NffSphere {
89        center,
90        radius,
91        surface,
92    });
93}
94
95/// Add a triangle mesh as polygons.
96pub fn nff_add_mesh(
97    doc: &mut NffDocument,
98    positions: &[[f32; 3]],
99    indices: &[u32],
100    surface: NffSurface,
101) {
102    for tri in indices.chunks(3) {
103        if tri.len() == 3 {
104            let verts = vec![
105                positions[tri[0] as usize],
106                positions[tri[1] as usize],
107                positions[tri[2] as usize],
108            ];
109            doc.polygons.push(NffPolygon {
110                vertices: verts,
111                surface: surface.clone(),
112            });
113        }
114    }
115}
116
117/// Return the light count.
118pub fn nff_light_count(doc: &NffDocument) -> usize {
119    doc.lights.len()
120}
121
122/// Return the polygon count.
123pub fn nff_polygon_count(doc: &NffDocument) -> usize {
124    doc.polygons.len()
125}
126
127/// Return the sphere count.
128pub fn nff_sphere_count(doc: &NffDocument) -> usize {
129    doc.spheres.len()
130}
131
132/// Return the total primitive count (polygons + spheres).
133pub fn nff_primitive_count(doc: &NffDocument) -> usize {
134    doc.polygons.len() + doc.spheres.len()
135}
136
137fn render_nff_surface(s: &NffSurface) -> String {
138    format!(
139        "f {:.4} {:.4} {:.4}  {:.4} {:.4} {:.4} {:.4} {:.4}\n",
140        s.color[0], s.color[1], s.color[2], s.kd, s.ks, s.shine, s.transmittance, s.ior
141    )
142}
143
144/// Render the NFF scene to a string.
145pub fn render_nff(doc: &NffDocument) -> String {
146    let mut out = String::from("# NFF scene — generated by oxihuman\n");
147    /* Background color */
148    out.push_str(&format!(
149        "b {:.4} {:.4} {:.4}\n",
150        doc.background[0], doc.background[1], doc.background[2]
151    ));
152    /* Lights */
153    for light in &doc.lights {
154        let [lx, ly, lz] = light.position;
155        let [lr, lg, lb] = light.color;
156        out.push_str(&format!(
157            "l {lx:.4} {ly:.4} {lz:.4} {lr:.4} {lg:.4} {lb:.4}\n"
158        ));
159    }
160    /* Spheres */
161    for sph in &doc.spheres {
162        out.push_str(&render_nff_surface(&sph.surface));
163        let [cx, cy, cz] = sph.center;
164        out.push_str(&format!("s {cx:.4} {cy:.4} {cz:.4} {:.4}\n", sph.radius));
165    }
166    /* Polygons */
167    for poly in &doc.polygons {
168        out.push_str(&render_nff_surface(&poly.surface));
169        out.push_str(&format!("p {}\n", poly.vertices.len()));
170        for v in &poly.vertices {
171            out.push_str(&format!("{:.6} {:.6} {:.6}\n", v[0], v[1], v[2]));
172        }
173    }
174    out
175}
176
177/// Estimate the file size.
178pub fn nff_size_estimate(doc: &NffDocument) -> usize {
179    render_nff(doc).len()
180}
181
182/// Validate the document (all polygons >= 3 vertices, sphere radii > 0).
183pub fn validate_nff(doc: &NffDocument) -> bool {
184    let polys_ok = doc.polygons.iter().all(|p| p.vertices.len() >= 3);
185    let spheres_ok = doc.spheres.iter().all(|s| s.radius > 0.0);
186    polys_ok && spheres_ok
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    fn simple_doc() -> NffDocument {
194        let mut doc = new_nff_document();
195        nff_add_light(&mut doc, [1.0, 2.0, 3.0], [1.0, 1.0, 1.0]);
196        nff_add_polygon(
197            &mut doc,
198            vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
199            NffSurface::default(),
200        );
201        nff_add_sphere(&mut doc, [0.0, 0.0, 0.0], 1.0, NffSurface::default());
202        doc
203    }
204
205    #[test]
206    fn light_count() {
207        assert_eq!(nff_light_count(&simple_doc()), 1);
208    }
209
210    #[test]
211    fn polygon_count() {
212        assert_eq!(nff_polygon_count(&simple_doc()), 1);
213    }
214
215    #[test]
216    fn sphere_count() {
217        assert_eq!(nff_sphere_count(&simple_doc()), 1);
218    }
219
220    #[test]
221    fn primitive_count() {
222        assert_eq!(nff_primitive_count(&simple_doc()), 2);
223    }
224
225    #[test]
226    fn render_starts_with_comment() {
227        let s = render_nff(&simple_doc());
228        assert!(s.starts_with("# NFF"));
229    }
230
231    #[test]
232    fn render_contains_background() {
233        let s = render_nff(&simple_doc());
234        assert!(s.contains("\nb "));
235    }
236
237    #[test]
238    fn render_contains_polygon_marker() {
239        let s = render_nff(&simple_doc());
240        assert!(s.contains("\np 3"));
241    }
242
243    #[test]
244    fn render_contains_sphere_marker() {
245        let s = render_nff(&simple_doc());
246        assert!(s.contains("\ns "));
247    }
248
249    #[test]
250    fn validate_valid_doc() {
251        assert!(validate_nff(&simple_doc()));
252    }
253
254    #[test]
255    fn add_mesh_creates_polygons() {
256        let mut doc = new_nff_document();
257        nff_add_mesh(
258            &mut doc,
259            &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
260            &[0, 1, 2],
261            NffSurface::default(),
262        );
263        assert_eq!(nff_polygon_count(&doc), 1);
264    }
265
266    #[test]
267    fn size_estimate_positive() {
268        assert!(nff_size_estimate(&simple_doc()) > 0);
269    }
270}