Skip to main content

quantized_mesh/
encoding.rs

1//! Encoding functions and main encoder for quantized-mesh format.
2//!
3//! The streaming writer uses inline zigzag-delta and high-water-mark
4//! encoding with a small chunked buffer, so no intermediate `Vec<u16>` /
5//! `Vec<u32>` is allocated for the encoded streams.
6
7use std::io::{self, Write};
8
9use flate2::Compression;
10use flate2::write::GzEncoder;
11
12use crate::{
13    EdgeIndices, ExtensionId, QuantizedMeshHeader, QuantizedVertices, TileMetadata, WaterMask,
14};
15
16/// Encode a value using zig-zag encoding.
17///
18/// Maps signed integers to unsigned integers so that small magnitude values
19/// (positive or negative) have small encoded values.
20///
21/// ```text
22/// 0 -> 0, -1 -> 1, 1 -> 2, -2 -> 3, 2 -> 4, ...
23/// ```
24#[inline]
25pub fn zigzag_encode(value: i32) -> u32 {
26    ((value << 1) ^ (value >> 31)) as u32
27}
28
29/// Decode a zig-zag encoded value.
30#[inline]
31pub fn zigzag_decode(value: u32) -> i32 {
32    ((value >> 1) as i32) ^ (-((value & 1) as i32))
33}
34
35/// Oct-encode a unit normal vector to 2 bytes.
36///
37/// Uses octahedron encoding for efficient normal compression.
38pub fn oct_encode_normal(normal: [f32; 3]) -> [u8; 2] {
39    let [mut x, mut y, z] = normal;
40
41    // Project to octahedron
42    let inv_l1 = 1.0 / (x.abs() + y.abs() + z.abs());
43    x *= inv_l1;
44    y *= inv_l1;
45
46    // Unfold lower hemisphere
47    if z < 0.0 {
48        let ox = x;
49        x = (1.0 - y.abs()) * if ox >= 0.0 { 1.0 } else { -1.0 };
50        y = (1.0 - ox.abs()) * if y >= 0.0 { 1.0 } else { -1.0 };
51    }
52
53    // Map from [-1, 1] to [0, 255]
54    let encode = |v: f32| -> u8 { ((v * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8 };
55
56    [encode(x), encode(y)]
57}
58
59/// Oct-encode a batch of unit normals, writing 2 bytes per normal into `out`.
60///
61/// Equivalent to calling [`oct_encode_normal`] on each normal and
62/// concatenating the results — the output is identical on every target.
63///
64/// On `wasm32` the batch is encoded with explicit simd128 (four normals per
65/// `f32x4` step), which benchmarked ~1.66× faster than the scalar loop under
66/// a WebAssembly runtime. On every other target it is the plain scalar loop:
67/// native back-ends autovectorise that form better than the hand-written
68/// SIMD, which regressed in native benchmarks, so the SIMD path is gated to
69/// wasm only.
70///
71/// # Panics
72///
73/// Panics if `out.len() != normals.len() * 2`.
74pub fn oct_encode_normals_into(normals: &[[f32; 3]], out: &mut [u8]) {
75    assert_eq!(
76        out.len(),
77        normals.len() * 2,
78        "normal output length mismatch: expected {}, got {}",
79        normals.len() * 2,
80        out.len()
81    );
82
83    #[cfg(not(target_arch = "wasm32"))]
84    for (n, o) in normals.iter().zip(out.chunks_exact_mut(2)) {
85        o.copy_from_slice(&oct_encode_normal(*n));
86    }
87
88    #[cfg(target_arch = "wasm32")]
89    oct_encode_normals_simd128(normals, out);
90}
91
92/// simd128 batch oct-encode. Bit-for-bit identical to [`oct_encode_normal`]:
93/// every step is IEEE add/sub/mul/div/abs/max/min plus lane selects, and the
94/// `as u8` truncation matches the scalar `clamp(..) as u8`.
95#[cfg(target_arch = "wasm32")]
96fn oct_encode_normals_simd128(normals: &[[f32; 3]], out: &mut [u8]) {
97    use wide::{CmpGe, CmpLt, f32x4};
98
99    let one = f32x4::splat(1.0);
100    let neg_one = f32x4::splat(-1.0);
101    let zero = f32x4::splat(0.0);
102    let half = f32x4::splat(0.5);
103    let n255 = f32x4::splat(255.0);
104
105    let mut nchunks = normals.chunks_exact(4);
106    let mut ochunks = out.chunks_exact_mut(8);
107    for (nc, oc) in nchunks.by_ref().zip(ochunks.by_ref()) {
108        let x = f32x4::new([nc[0][0], nc[1][0], nc[2][0], nc[3][0]]);
109        let y = f32x4::new([nc[0][1], nc[1][1], nc[2][1], nc[3][1]]);
110        let z = f32x4::new([nc[0][2], nc[1][2], nc[2][2], nc[3][2]]);
111
112        let inv_l1 = one / (x.abs() + y.abs() + z.abs());
113        let px = x * inv_l1;
114        let py = y * inv_l1;
115
116        // Unfold lower hemisphere where z < 0 (branchless via select).
117        let neg = z.cmp_lt(zero);
118        let sign_x = px.cmp_ge(zero).blend(one, neg_one);
119        let sign_y = py.cmp_ge(zero).blend(one, neg_one);
120        let fx = (one - py.abs()) * sign_x;
121        let fy = (one - px.abs()) * sign_y;
122        let px = neg.blend(fx, px);
123        let py = neg.blend(fy, py);
124
125        // Map [-1,1] → [0,255], clamp, truncate to u8.
126        let bx = ((px * half + half) * n255).max(zero).min(n255).to_array();
127        let by = ((py * half + half) * n255).max(zero).min(n255).to_array();
128        for k in 0..4 {
129            oc[k * 2] = bx[k] as u8;
130            oc[k * 2 + 1] = by[k] as u8;
131        }
132    }
133
134    // Scalar remainder for the ≤3 trailing normals.
135    for (n, o) in nchunks
136        .remainder()
137        .iter()
138        .zip(ochunks.into_remainder().chunks_exact_mut(2))
139    {
140        o.copy_from_slice(&oct_encode_normal(*n));
141    }
142}
143
144/// Options for encoding quantized mesh.
145#[derive(Debug, Clone, Default)]
146pub struct EncodeOptions {
147    /// Include oct-encoded vertex normals
148    pub include_normals: bool,
149    /// Vertex normals (required if include_normals is true)
150    pub normals: Option<Vec<[f32; 3]>>,
151    /// Include water mask
152    pub include_water_mask: bool,
153    /// Water mask data
154    pub water_mask: Option<WaterMask>,
155    /// Include metadata extension with tile availability
156    pub include_metadata: bool,
157    /// Metadata for tile availability
158    pub metadata: Option<TileMetadata>,
159    /// Gzip compression level (0-9, default 6)
160    pub compression_level: u32,
161}
162
163/// Quantized mesh encoder.
164///
165/// Encodes terrain mesh data into the quantized-mesh-1.0 format.
166pub struct QuantizedMeshEncoder {
167    header: QuantizedMeshHeader,
168    vertices: QuantizedVertices,
169    indices: Vec<u32>,
170    edge_indices: EdgeIndices,
171}
172
173impl QuantizedMeshEncoder {
174    /// Create a new encoder with mesh data.
175    pub fn new(
176        header: QuantizedMeshHeader,
177        vertices: QuantizedVertices,
178        indices: Vec<u32>,
179        edge_indices: EdgeIndices,
180    ) -> Self {
181        Self {
182            header,
183            vertices,
184            indices,
185            edge_indices,
186        }
187    }
188
189    /// Encode to quantized-mesh format without compression.
190    pub fn encode(&self) -> Vec<u8> {
191        self.encode_with_options(&EncodeOptions::default())
192    }
193
194    /// Encode with options (extensions, compression).
195    pub fn encode_with_options(&self, options: &EncodeOptions) -> Vec<u8> {
196        let mut output = Vec::new();
197        self.encode_to_with_options(&mut output, options)
198            .expect("Failed to encode to Vec");
199        output
200    }
201
202    /// Encode to a writer without compression.
203    pub fn encode_to<W: Write>(&self, writer: W) -> io::Result<()> {
204        self.encode_to_with_options(writer, &EncodeOptions::default())
205    }
206
207    /// Encode to a writer with options (extensions, compression).
208    pub fn encode_to_with_options<W: Write>(
209        &self,
210        writer: W,
211        options: &EncodeOptions,
212    ) -> io::Result<()> {
213        if options.compression_level == 0 {
214            self.encode_uncompressed_to(writer, options)
215        } else {
216            let mut encoder = GzEncoder::new(writer, Compression::new(options.compression_level));
217            self.encode_uncompressed_to(&mut encoder, options)?;
218            encoder.finish()?;
219            Ok(())
220        }
221    }
222
223    /// Encode without compression to a writer, streaming each section
224    /// directly without intermediate Vec allocations.
225    fn encode_uncompressed_to<W: Write>(
226        &self,
227        mut writer: W,
228        options: &EncodeOptions,
229    ) -> io::Result<()> {
230        let vertex_count = self.vertices.len();
231        let use_32bit = vertex_count > 65535;
232
233        // Header (88 bytes).
234        writer.write_all(&self.header.to_bytes())?;
235        // Vertex count.
236        writer.write_all(&(vertex_count as u32).to_le_bytes())?;
237
238        // Vertex u/v/height streams (zigzag-delta).
239        write_zigzag_delta_to(&mut writer, &self.vertices.u)?;
240        write_zigzag_delta_to(&mut writer, &self.vertices.v)?;
241        write_zigzag_delta_to(&mut writer, &self.vertices.height)?;
242
243        // Pad to index alignment. After header+count+vertices the offset is:
244        let current_pos = 88 + 4 + vertex_count * 6;
245        let align = if use_32bit { 4 } else { 2 };
246        let padding = (align - (current_pos % align)) % align;
247        if padding > 0 {
248            let zeros = [0u8; 4];
249            writer.write_all(&zeros[..padding])?;
250        }
251
252        // Triangle count + high-water-mark indices.
253        let triangle_count = self.indices.len() / 3;
254        writer.write_all(&(triangle_count as u32).to_le_bytes())?;
255        write_high_water_mark_to(&mut writer, &self.indices, use_32bit)?;
256
257        // Edge index streams.
258        for edge in [
259            &self.edge_indices.west,
260            &self.edge_indices.south,
261            &self.edge_indices.east,
262            &self.edge_indices.north,
263        ] {
264            writer.write_all(&(edge.len() as u32).to_le_bytes())?;
265            write_indices_to(&mut writer, edge, use_32bit)?;
266        }
267
268        // Extensions.
269        if options.include_normals
270            && let Some(normals) = &options.normals
271        {
272            write_normals_extension_to(&mut writer, normals)?;
273        }
274        if options.include_water_mask {
275            let water_mask = options.water_mask.as_ref().cloned().unwrap_or_default();
276            write_water_mask_extension_to(&mut writer, &water_mask)?;
277        }
278        if options.include_metadata
279            && let Some(metadata) = &options.metadata
280        {
281            write_metadata_extension_to(&mut writer, metadata)?;
282        }
283
284        Ok(())
285    }
286}
287
288// ---------------------------------------------------------------------------
289// Streaming write helpers
290// ---------------------------------------------------------------------------
291
292/// Buffer size for streaming writes. 4 KiB = 2048 u16s or 1024 u32s — large
293/// enough to amortise per-call `write_all` overhead.
294const WRITE_BUF: usize = 4096;
295
296fn write_zigzag_delta_to<W: Write>(writer: &mut W, values: &[u16]) -> io::Result<()> {
297    let mut buf = [0u8; WRITE_BUF];
298    let mut len = 0;
299    let mut prev = 0i32;
300    for &value in values {
301        let current = value as i32;
302        let delta = current - prev;
303        let bytes = (zigzag_encode(delta) as u16).to_le_bytes();
304        buf[len] = bytes[0];
305        buf[len + 1] = bytes[1];
306        len += 2;
307        prev = current;
308        if len + 2 > WRITE_BUF {
309            writer.write_all(&buf[..len])?;
310            len = 0;
311        }
312    }
313    if len > 0 {
314        writer.write_all(&buf[..len])?;
315    }
316    Ok(())
317}
318
319fn write_high_water_mark_to<W: Write>(
320    writer: &mut W,
321    indices: &[u32],
322    use_32bit: bool,
323) -> io::Result<()> {
324    let mut buf = [0u8; WRITE_BUF];
325    let mut len = 0;
326    let mut highest = 0u32;
327    let stride = if use_32bit { 4 } else { 2 };
328    for &index in indices {
329        let code = if index == highest {
330            highest += 1;
331            0
332        } else {
333            highest - index
334        };
335        if use_32bit {
336            buf[len..len + 4].copy_from_slice(&code.to_le_bytes());
337        } else {
338            buf[len..len + 2].copy_from_slice(&(code as u16).to_le_bytes());
339        }
340        len += stride;
341        if len + stride > WRITE_BUF {
342            writer.write_all(&buf[..len])?;
343            len = 0;
344        }
345    }
346    if len > 0 {
347        writer.write_all(&buf[..len])?;
348    }
349    Ok(())
350}
351
352fn write_indices_to<W: Write>(writer: &mut W, indices: &[u32], use_32bit: bool) -> io::Result<()> {
353    let mut buf = [0u8; WRITE_BUF];
354    let mut len = 0;
355    let stride = if use_32bit { 4 } else { 2 };
356    for &idx in indices {
357        if use_32bit {
358            buf[len..len + 4].copy_from_slice(&idx.to_le_bytes());
359        } else {
360            buf[len..len + 2].copy_from_slice(&(idx as u16).to_le_bytes());
361        }
362        len += stride;
363        if len + stride > WRITE_BUF {
364            writer.write_all(&buf[..len])?;
365            len = 0;
366        }
367    }
368    if len > 0 {
369        writer.write_all(&buf[..len])?;
370    }
371    Ok(())
372}
373
374fn write_normals_extension_to<W: Write>(writer: &mut W, normals: &[[f32; 3]]) -> io::Result<()> {
375    writer.write_all(&[ExtensionId::OctEncodedVertexNormals as u8])?;
376    writer.write_all(&((normals.len() * 2) as u32).to_le_bytes())?;
377    // Encode up to WRITE_BUF/2 normals per flush via the batch encoder
378    // (simd128 on wasm, scalar elsewhere). WRITE_BUF is even.
379    let mut buf = [0u8; WRITE_BUF];
380    for chunk in normals.chunks(WRITE_BUF / 2) {
381        let n = chunk.len() * 2;
382        oct_encode_normals_into(chunk, &mut buf[..n]);
383        writer.write_all(&buf[..n])?;
384    }
385    Ok(())
386}
387
388fn write_water_mask_extension_to<W: Write>(
389    writer: &mut W,
390    water_mask: &WaterMask,
391) -> io::Result<()> {
392    writer.write_all(&[ExtensionId::WaterMask as u8])?;
393    match water_mask {
394        WaterMask::Uniform(value) => {
395            writer.write_all(&1u32.to_le_bytes())?;
396            writer.write_all(&[*value])?;
397        }
398        WaterMask::Grid(grid) => {
399            writer.write_all(&(256 * 256u32).to_le_bytes())?;
400            writer.write_all(grid.as_ref())?;
401        }
402    }
403    Ok(())
404}
405
406fn write_metadata_extension_to<W: Write>(
407    writer: &mut W,
408    metadata: &TileMetadata,
409) -> io::Result<()> {
410    let json = serde_json::to_string(metadata)
411        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
412    let json_bytes = json.as_bytes();
413    writer.write_all(&[ExtensionId::Metadata as u8])?;
414    let extension_length = 4 + json_bytes.len() as u32;
415    writer.write_all(&extension_length.to_le_bytes())?;
416    writer.write_all(&(json_bytes.len() as u32).to_le_bytes())?;
417    writer.write_all(json_bytes)?;
418    Ok(())
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    /// Spread of unit normals over both hemispheres; `n` need not be a
426    /// multiple of 4 (exercises the SIMD remainder tail).
427    fn spread_normals(n: usize) -> Vec<[f32; 3]> {
428        (0..n)
429            .map(|i| {
430                let a = (i as f32) * 0.61803398875;
431                let b = (i as f32) * 0.15915494309;
432                let x = a.sin() * 0.9;
433                let y = b.cos() * 0.9;
434                let z = 1.0 - x.abs() - y.abs();
435                let len = (x * x + y * y + z * z).sqrt().max(1e-6);
436                [x / len, y / len, z / len]
437            })
438            .collect()
439    }
440
441    #[test]
442    fn oct_encode_normals_into_matches_per_normal() {
443        let normals = spread_normals(1003); // 1003 % 4 == 3
444        let mut batch = vec![0u8; normals.len() * 2];
445        oct_encode_normals_into(&normals, &mut batch);
446        for (i, n) in normals.iter().enumerate() {
447            assert_eq!(
448                [batch[i * 2], batch[i * 2 + 1]],
449                oct_encode_normal(*n),
450                "batch encode mismatch at normal {i}"
451            );
452        }
453    }
454
455    #[test]
456    fn test_zigzag_encode() {
457        assert_eq!(zigzag_encode(0), 0);
458        assert_eq!(zigzag_encode(-1), 1);
459        assert_eq!(zigzag_encode(1), 2);
460        assert_eq!(zigzag_encode(-2), 3);
461        assert_eq!(zigzag_encode(2), 4);
462    }
463
464    #[test]
465    fn test_zigzag_roundtrip() {
466        for i in -1000..1000 {
467            assert_eq!(zigzag_decode(zigzag_encode(i)), i);
468        }
469    }
470
471    #[test]
472    fn test_oct_encode_normal() {
473        let up = [0.0f32, 0.0, 1.0];
474        let encoded = oct_encode_normal(up);
475        assert!((encoded[0] as i32 - 127).abs() < 2);
476        assert!((encoded[1] as i32 - 127).abs() < 2);
477
478        let down = [0.0f32, 0.0, -1.0];
479        let encoded = oct_encode_normal(down);
480        assert!(encoded[0] == 0 || encoded[0] == 255);
481    }
482
483    #[test]
484    fn test_encoder_basic() {
485        let header = QuantizedMeshHeader::default();
486        let vertices = QuantizedVertices {
487            u: vec![0, 32767, 0, 32767],
488            v: vec![0, 0, 32767, 32767],
489            height: vec![0, 0, 0, 0],
490        };
491        let indices = vec![0, 1, 2, 1, 3, 2];
492        let edge_indices = EdgeIndices::from_vertices(&vertices);
493
494        let encoder = QuantizedMeshEncoder::new(header, vertices, indices, edge_indices);
495        let data = encoder.encode_with_options(&EncodeOptions {
496            compression_level: 0,
497            ..Default::default()
498        });
499
500        assert!(data.len() > 88);
501        let parsed_header = QuantizedMeshHeader::from_bytes(&data).unwrap();
502        assert_eq!(parsed_header.min_height, 0.0);
503    }
504
505    #[test]
506    fn test_encoder_with_compression() {
507        let header = QuantizedMeshHeader::default();
508        let vertices = QuantizedVertices {
509            u: vec![0, 32767, 0, 32767],
510            v: vec![0, 0, 32767, 32767],
511            height: vec![0, 0, 0, 0],
512        };
513        let indices = vec![0, 1, 2, 1, 3, 2];
514        let edge_indices = EdgeIndices::from_vertices(&vertices);
515
516        let encoder = QuantizedMeshEncoder::new(header, vertices, indices, edge_indices);
517
518        let compressed = encoder.encode_with_options(&EncodeOptions {
519            compression_level: 6,
520            ..Default::default()
521        });
522
523        assert_eq!(&compressed[0..2], &[0x1f, 0x8b]);
524    }
525
526    #[test]
527    fn test_encoder_with_extensions() {
528        let header = QuantizedMeshHeader::default();
529        let vertices = QuantizedVertices {
530            u: vec![0, 32767, 0, 32767],
531            v: vec![0, 0, 32767, 32767],
532            height: vec![0, 0, 0, 0],
533        };
534        let indices = vec![0, 1, 2, 1, 3, 2];
535        let edge_indices = EdgeIndices::from_vertices(&vertices);
536
537        let encoder = QuantizedMeshEncoder::new(header, vertices, indices, edge_indices);
538
539        let normals = vec![[0.0, 0.0, 1.0]; 4];
540
541        let data = encoder.encode_with_options(&EncodeOptions {
542            compression_level: 0,
543            include_normals: true,
544            normals: Some(normals),
545            include_water_mask: true,
546            water_mask: Some(WaterMask::Uniform(0)),
547            ..Default::default()
548        });
549
550        let without_ext = encoder.encode_with_options(&EncodeOptions {
551            compression_level: 0,
552            ..Default::default()
553        });
554
555        assert!(data.len() > without_ext.len());
556    }
557
558    #[test]
559    fn test_encode_to_writer_matches_encode_with_options() {
560        let header = QuantizedMeshHeader::default();
561        let vertices = QuantizedVertices {
562            u: vec![0, 32767, 0, 32767],
563            v: vec![0, 0, 32767, 32767],
564            height: vec![0, 0, 0, 0],
565        };
566        let indices = vec![0, 1, 2, 1, 3, 2];
567        let edge_indices = EdgeIndices::from_vertices(&vertices);
568
569        let encoder = QuantizedMeshEncoder::new(header, vertices, indices, edge_indices);
570
571        let data_vec = encoder.encode_with_options(&EncodeOptions {
572            compression_level: 0,
573            ..Default::default()
574        });
575
576        let mut data_writer = Vec::new();
577        encoder
578            .encode_to_with_options(
579                &mut data_writer,
580                &EncodeOptions {
581                    compression_level: 0,
582                    ..Default::default()
583                },
584            )
585            .expect("Failed to encode to writer");
586
587        assert_eq!(data_vec, data_writer);
588    }
589
590    #[test]
591    fn test_encode_to_writer_compressed() {
592        let header = QuantizedMeshHeader::default();
593        let vertices = QuantizedVertices {
594            u: vec![0, 32767, 0, 32767],
595            v: vec![0, 0, 32767, 32767],
596            height: vec![0, 0, 0, 0],
597        };
598        let indices = vec![0, 1, 2, 1, 3, 2];
599        let edge_indices = EdgeIndices::from_vertices(&vertices);
600
601        let encoder = QuantizedMeshEncoder::new(header, vertices, indices, edge_indices);
602
603        let mut data_writer = Vec::new();
604        encoder
605            .encode_to_with_options(
606                &mut data_writer,
607                &EncodeOptions {
608                    compression_level: 6,
609                    ..Default::default()
610                },
611            )
612            .expect("Failed to encode to writer");
613
614        assert_eq!(&data_writer[0..2], &[0x1f, 0x8b]);
615    }
616}