draco-core 2.0.0

Pure Rust core encoder and decoder for Draco geometry compression
Documentation
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
use draco_core::decoder_buffer::DecoderBuffer;
use draco_core::draco_types::DataType;
use draco_core::encoder_buffer::EncoderBuffer;
use draco_core::encoder_options::EncoderOptions;
use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
use draco_core::geometry_indices::{FaceIndex, PointIndex};
use draco_core::mesh::Mesh;
use draco_core::mesh_decoder::MeshDecoder;
use draco_core::mesh_encoder::MeshEncoder;

fn create_grid_mesh(width: u32, height: u32) -> Mesh {
    let mut mesh = Mesh::new();
    let num_points = width * height;
    mesh.set_num_points(num_points as usize);

    let mut pos_attr = PointAttribute::new();
    pos_attr.init(
        GeometryAttributeType::Position,
        3,
        DataType::Float32,
        false,
        num_points as usize,
    );

    for y in 0..height {
        for x in 0..width {
            let i = y * width + x;
            let coords = [x as f32, y as f32, 0.0f32];
            let offset = (i as usize) * 3 * 4;
            pos_attr
                .buffer_mut()
                .update(&coords[0].to_le_bytes(), Some(offset));
            pos_attr
                .buffer_mut()
                .update(&coords[1].to_le_bytes(), Some(offset + 4));
            pos_attr
                .buffer_mut()
                .update(&coords[2].to_le_bytes(), Some(offset + 8));
        }
    }
    mesh.add_attribute(pos_attr);

    // Create faces (2 triangles per grid cell)
    let mut face_idx = 0;
    for y in 0..height - 1 {
        for x in 0..width - 1 {
            let p0 = y * width + x;
            let p1 = y * width + (x + 1);
            let p2 = (y + 1) * width + x;
            let p3 = (y + 1) * width + (x + 1);

            // Triangle 1: p0, p1, p2
            mesh.set_face(
                FaceIndex(face_idx),
                [PointIndex(p0), PointIndex(p1), PointIndex(p2)],
            );
            face_idx += 1;

            // Triangle 2: p1, p3, p2
            mesh.set_face(
                FaceIndex(face_idx),
                [PointIndex(p1), PointIndex(p3), PointIndex(p2)],
            );
            face_idx += 1;
        }
    }
    mesh.set_num_faces(face_idx as usize);

    mesh
}

fn verify_mesh_attributes(original: &Mesh, decoded: &Mesh, max_error: f32) {
    // Edgebreaker may introduce split vertices, so decoded count >= original count
    assert!(
        decoded.num_points() >= original.num_points(),
        "Decoded points {} < Original points {}",
        decoded.num_points(),
        original.num_points()
    );

    let orig_attr = original.attribute(0);
    let dec_attr = decoded.attribute(0);

    let orig_data = orig_attr.buffer().data();
    let dec_data = dec_attr.buffer().data();

    // Collect all decoded points
    let mut decoded_points = Vec::new();
    println!("Decoded Points (total: {}):", decoded.num_points());
    for i in 0..decoded.num_points() {
        let offset = i * 3 * 4;
        let dx = f32::from_le_bytes(dec_data[offset..offset + 4].try_into().unwrap());
        let dy = f32::from_le_bytes(dec_data[offset + 4..offset + 8].try_into().unwrap());
        let dz = f32::from_le_bytes(dec_data[offset + 8..offset + 12].try_into().unwrap());
        decoded_points.push([dx, dy, dz]);
    }
    // Print statistics
    let min_x = decoded_points
        .iter()
        .map(|p| p[0])
        .fold(f32::INFINITY, f32::min);
    let max_x = decoded_points
        .iter()
        .map(|p| p[0])
        .fold(f32::NEG_INFINITY, f32::max);
    let min_y = decoded_points
        .iter()
        .map(|p| p[1])
        .fold(f32::INFINITY, f32::min);
    let max_y = decoded_points
        .iter()
        .map(|p| p[1])
        .fold(f32::NEG_INFINITY, f32::max);
    println!(
        "  Point range: x=[{:.3}, {:.3}], y=[{:.3}, {:.3}]",
        min_x, max_x, min_y, max_y
    );
    // Print first 20 decoded point values
    println!("Decoded point values (first 20):");
    for (i, p) in decoded_points.iter().enumerate().take(20) {
        println!("  Point {}: ({:.3}, {:.3}, {:.3})", i, p[0], p[1], p[2]);
    }
    println!("Decoded faces (total: {}):", decoded.num_faces());
    for i in 0..std::cmp::min(5, decoded.num_faces()) {
        let face = decoded.face(FaceIndex(i as u32));
        println!("  Face {}: {:?}", i, face);
    }

    fn round_f32_to_i32(v: f32) -> i32 {
        // Grid tests use non-negative coordinates; round() is sufficient.
        v.round() as i32
    }

    // Verify each original point exists in decoded points
    for i in 0..original.num_points() {
        let offset = i * 3 * 4;
        let ox = f32::from_le_bytes(orig_data[offset..offset + 4].try_into().unwrap());
        let oy = f32::from_le_bytes(orig_data[offset + 4..offset + 8].try_into().unwrap());
        let oz = f32::from_le_bytes(orig_data[offset + 8..offset + 12].try_into().unwrap());

        let rox = round_f32_to_i32(ox);
        let roy = round_f32_to_i32(oy);
        let roz = round_f32_to_i32(oz);

        let mut found = false;
        for dp in &decoded_points {
            // Primary match: compare rounded coordinates.
            if round_f32_to_i32(dp[0]) == rox
                && round_f32_to_i32(dp[1]) == roy
                && round_f32_to_i32(dp[2]) == roz
            {
                found = true;
                break;
            }

            // Fallback: max-error comparison for non-grid uses.
            if (ox - dp[0]).abs() <= max_error
                && (oy - dp[1]).abs() <= max_error
                && (oz - dp[2]).abs() <= max_error
            {
                found = true;
                break;
            }
        }
        assert!(
            found,
            "Point {} ({}, {}, {}) not found in decoded mesh",
            i, ox, oy, oz
        );
    }
}

#[test]
// #[ignore]
fn test_grid_encoding_parallelogram() {
    // Use 5x5 grid for easier comparison with C++
    let mesh = create_grid_mesh(5, 5);

    let mut options = EncoderOptions::default();
    options.set_global_int("encoding_method", 1); // Edgebreaker
    options.set_global_int("encoding_speed", 5); // Should select Parallelogram
    options.set_attribute_int(0, "quantization_bits", 10); // Match C++ -qp 10

    let mut encoder = MeshEncoder::new();
    encoder.set_mesh(mesh.clone());
    let mut buffer = EncoderBuffer::new();
    encoder
        .encode(&options, &mut buffer)
        .expect("Encode failed");

    println!("Parallelogram encoded size: {}", buffer.data().len());

    let mut decoder = MeshDecoder::new();
    let mut decoded_mesh = Mesh::new();
    let mut decoder_buffer = DecoderBuffer::new(buffer.data());
    decoder
        .decode(&mut decoder_buffer, &mut decoded_mesh)
        .expect("Decode failed");

    // With 10 bits quantization on range [0, 4], error should be very small.
    verify_mesh_attributes(&mesh, &decoded_mesh, 0.01);
}

#[test]
fn test_grid_encoding_difference() {
    let mesh = create_grid_mesh(10, 10);

    let mut options = EncoderOptions::default();
    options.set_global_int("encoding_method", 1); // Edgebreaker
    options.set_global_int("encoding_speed", 10); // Should select Difference
    options.set_attribute_int(0, "quantization_bits", 14);

    let mut encoder = MeshEncoder::new();
    encoder.set_mesh(mesh.clone());
    let mut buffer = EncoderBuffer::new();
    encoder
        .encode(&options, &mut buffer)
        .expect("Encode failed");

    println!("Difference encoded size: {}", buffer.data().len());

    let mut decoder = MeshDecoder::new();
    let mut decoded_mesh = Mesh::new();
    let mut decoder_buffer = DecoderBuffer::new(buffer.data());
    decoder
        .decode(&mut decoder_buffer, &mut decoded_mesh)
        .expect("Decode failed");

    verify_mesh_attributes(&mesh, &decoded_mesh, 0.01);
}

#[test]
fn test_quantization_levels() {
    let mesh = create_grid_mesh(5, 5);

    let q_levels = [8, 10, 16];

    for &q in &q_levels {
        let mut options = EncoderOptions::default();
        options.set_global_int("encoding_method", 1);
        options.set_attribute_int(0, "quantization_bits", q);

        let mut encoder = MeshEncoder::new();
        encoder.set_mesh(mesh.clone());
        let mut buffer = EncoderBuffer::new();
        encoder
            .encode(&options, &mut buffer)
            .expect("Encode failed");

        let mut decoder = MeshDecoder::new();
        let mut decoded_mesh = Mesh::new();
        let mut decoder_buffer = DecoderBuffer::new(buffer.data());
        decoder
            .decode(&mut decoder_buffer, &mut decoded_mesh)
            .expect("Decode failed");

        // Range is 4.0.
        // Error bound = Range / (2^q - 1)
        let range = 4.0;
        let max_error = range / ((1 << q) as f32 - 1.0);
        // Allow a bit of slack for float precision
        verify_mesh_attributes(&mesh, &decoded_mesh, max_error * 1.5);
    }
}

#[test]
fn test_grid_encoding_sequential() {
    let mesh = create_grid_mesh(10, 10);

    let mut options = EncoderOptions::default();
    options.set_global_int("encoding_method", 0); // Sequential
    options.set_global_int("encoding_speed", 5); // Parallelogram
    options.set_attribute_int(0, "quantization_bits", 14);

    let mut encoder = MeshEncoder::new();
    encoder.set_mesh(mesh.clone());
    let mut buffer = EncoderBuffer::new();
    encoder
        .encode(&options, &mut buffer)
        .expect("Encode failed");

    println!("Sequential encoded size: {}", buffer.data().len());

    let mut decoder = MeshDecoder::new();
    let mut decoded_mesh = Mesh::new();
    let mut decoder_buffer = DecoderBuffer::new(buffer.data());
    decoder
        .decode(&mut decoder_buffer, &mut decoded_mesh)
        .expect("Decode failed");

    verify_mesh_attributes(&mesh, &decoded_mesh, 0.002);
}

/// A `uint32` attribute keeps every bit through the round trip, including the
/// values above `i32::MAX` that upstream refuses to encode at all.
///
/// This is a deliberate widening, not an accident: upstream converts each value
/// with `ConvertValue<int32_t>` and fails the encode when one does not fit
/// (`ConvertComponentValue`, `geometry_attribute.h`), while this encoder carries
/// the bits through its `int32` portable attribute and the decoder writes them
/// back under the attribute's declared type. Upstream's own decoder reads such a
/// stream correctly -- its `StoreTypedValues` is a plain `static_cast` with no
/// range check -- so the wider domain stays readable by C++ Draco.
#[test]
fn a_uint32_attribute_keeps_values_above_i32_max_through_a_round_trip() {
    const WIDTH: u32 = 5;
    const HEIGHT: u32 = 5;
    const NUM_POINTS: usize = (WIDTH * HEIGHT) as usize;
    // Above `i32::MAX`, so the portable `int32` holds these bits as negatives.
    const BASE: u32 = 0xFFFF_0000;

    let mut mesh = Mesh::new();
    mesh.set_num_points(NUM_POINTS);

    let mut positions = PointAttribute::new();
    positions.init(
        GeometryAttributeType::Position,
        3,
        DataType::Uint32,
        false,
        NUM_POINTS,
    );
    let mut authored = Vec::with_capacity(NUM_POINTS * 3);
    for y in 0..HEIGHT {
        for x in 0..WIDTH {
            let index = (y * WIDTH + x) as usize;
            // Straddling `i32::MAX` is what makes the two readings disagree:
            // a difference between a value below the boundary and one above it
            // is 2^32 apart signed versus unsigned, which is what the portable
            // texture-coordinate predictor works on.
            let value = if (x + y) % 2 == 0 {
                [BASE + x * 16, BASE + y * 16, BASE + (x + y) * 4]
            } else {
                [x * 16, y * 16, (x + y) * 4]
            };
            for (component, scalar) in value.iter().enumerate() {
                positions
                    .buffer_mut()
                    .write((index * 3 + component) * 4, &scalar.to_le_bytes());
            }
            authored.extend_from_slice(&value);
        }
    }
    mesh.add_attribute(positions);

    // A texture coordinate predicted by the portable scheme reads the position
    // as a number rather than as storage, which is the path that used to
    // disagree between the two halves.
    let mut tex_coords = PointAttribute::new();
    tex_coords.init(
        GeometryAttributeType::TexCoord,
        2,
        DataType::Uint16,
        false,
        NUM_POINTS,
    );
    for point in 0..NUM_POINTS {
        let u = (point as u16).wrapping_mul(701);
        let v = (point as u16).wrapping_mul(263);
        tex_coords.buffer_mut().write(point * 4, &u.to_le_bytes());
        tex_coords
            .buffer_mut()
            .write(point * 4 + 2, &v.to_le_bytes());
    }
    mesh.add_attribute(tex_coords);

    let mut faces = Vec::new();
    for y in 0..HEIGHT - 1 {
        for x in 0..WIDTH - 1 {
            let i = y * WIDTH + x;
            faces.push([i, i + 1, i + WIDTH]);
            faces.push([i + 1, i + WIDTH + 1, i + WIDTH]);
        }
    }
    mesh.try_set_num_faces(faces.len()).expect("face count");
    for (index, face) in faces.iter().enumerate() {
        mesh.set_face_from_indices(index, *face);
    }

    let mut options = EncoderOptions::new();
    options.set_attribute_int(1, "prediction_scheme", 5);

    let mut encoder = MeshEncoder::new();
    encoder.set_mesh(mesh);
    let mut buffer = EncoderBuffer::new();
    encoder
        .encode(&options, &mut buffer)
        .expect("a uint32 attribute above i32::MAX encodes");

    let mut decoded = Mesh::new();
    MeshDecoder::new()
        .decode(&mut DecoderBuffer::new(buffer.data()), &mut decoded)
        .expect("and decodes again");

    let attribute = decoded
        .named_attribute(GeometryAttributeType::Position)
        .expect("decoded position attribute");
    assert_eq!(attribute.data_type(), DataType::Uint32);
    // Draco reorders points, so the values are compared as a set: what is
    // pinned here is that every authored bit pattern survives, not where it
    // lands.
    let mut decoded_values = Vec::with_capacity(NUM_POINTS);
    for point in 0..NUM_POINTS {
        let value_index = attribute.mapped_index(PointIndex(point as u32));
        let mut triple = [0u32; 3];
        for (component, scalar) in triple.iter_mut().enumerate() {
            let mut bytes = [0u8; 4];
            attribute.buffer().read(
                value_index.0 as usize * attribute.byte_stride() as usize + component * 4,
                &mut bytes,
            );
            *scalar = u32::from_le_bytes(bytes);
        }
        decoded_values.push(triple);
    }
    decoded_values.sort_unstable();

    let mut authored_values: Vec<[u32; 3]> = authored.as_chunks::<3>().0.to_vec();
    authored_values.sort_unstable();

    assert_eq!(
        decoded_values, authored_values,
        "a uint32 value above i32::MAX did not survive the round trip"
    );
    assert!(
        authored_values
            .iter()
            .flatten()
            .any(|&v| v > i32::MAX as u32),
        "the fixture has to carry values upstream would refuse, or it pins nothing"
    );
}