Skip to main content

draco_oxide/encode/attribute/portabilization/
mod.rs

1pub mod octahedral_quantization;
2pub mod quantization_coordinate_wise;
3pub mod to_bits;
4
5use draco_oxide_core::attribute::{Attribute, AttributeType};
6use draco_oxide_core::bit_coder::ByteWriter;
7use draco_oxide_core::codec::attribute::Portable;
8use draco_oxide_core::debug_write;
9use draco_oxide_core::types::NdVector;
10use draco_oxide_core::types::{ConfigType, Vector};
11
12pub enum Portabilization<Data, const N: usize>
13where
14    Data: Vector<N> + Portable,
15{
16    QuantizationCoordinateWise(quantization_coordinate_wise::QuantizationCoordinateWise<Data, N>),
17    OctahedralQuantization(octahedral_quantization::OctahedralQuantization<Data, N>),
18    ToBits(to_bits::ToBits<Data, N>),
19}
20
21impl<Data, const N: usize> Portabilization<Data, N>
22where
23    Data: Vector<N> + Portable,
24    NdVector<N, i32>: Vector<N, Component = i32>,
25    NdVector<N, f32>: Vector<N, Component = f32> + Portable,
26{
27    /// creates a new instance of the portabilization, computes the metadata, and
28    /// writes the metadata to the stream.
29    // enum_dispatch does not support associated functions, we explicitly write the
30    // constructor.
31    pub fn new<W>(att: Attribute, cfg: Config, writer: &mut W) -> Self
32    where
33        W: ByteWriter,
34    {
35        debug_write!("Start of Portabilization Metadata", writer);
36        // cfg.type_.write_to(writer);
37        let out = match cfg.type_ {
38            PortabilizationType::QuantizationCoordinateWise => {
39                Portabilization::QuantizationCoordinateWise(
40                    quantization_coordinate_wise::QuantizationCoordinateWise::<_, N>::new(
41                        att, cfg, writer,
42                    ),
43                )
44            }
45            PortabilizationType::OctahedralQuantization => Portabilization::OctahedralQuantization(
46                octahedral_quantization::OctahedralQuantization::new(att, cfg, writer),
47            ),
48            PortabilizationType::ToBits => {
49                Portabilization::ToBits(to_bits::ToBits::new(att, cfg, writer))
50            }
51            PortabilizationType::Integer => {
52                unimplemented!("Integer portabilization is not implemented yet.")
53            }
54        };
55        debug_write!("End of Portabilization Metadata", writer);
56        out
57    }
58
59    pub fn portabilize(self) -> Attribute {
60        match self {
61            Portabilization::QuantizationCoordinateWise(qcw) => qcw.portabilize(),
62            Portabilization::OctahedralQuantization(oct) => oct.portabilize(),
63            Portabilization::ToBits(tb) => tb.portabilize(),
64        }
65    }
66}
67
68pub trait PortabilizationImpl<const N: usize>
69where
70    NdVector<N, i32>: Vector<N, Component = i32>,
71{
72    /// portabilizes the whole data.
73    fn portabilize(self) -> Attribute;
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum PortabilizationType {
78    QuantizationCoordinateWise,
79    OctahedralQuantization,
80    #[allow(dead_code)]
81    Integer,
82    ToBits,
83}
84
85impl PortabilizationType {
86    pub(crate) fn get_id(&self) -> u8 {
87        match self {
88            PortabilizationType::ToBits => 1,
89            PortabilizationType::Integer => 1, // Integer is not used in the current implementation, but kept for compatibility.
90            PortabilizationType::QuantizationCoordinateWise => 2,
91            PortabilizationType::OctahedralQuantization => 3,
92        }
93    }
94
95    pub(crate) fn write_to<W>(&self, writer: &mut W)
96    where
97        W: ByteWriter,
98    {
99        let id = self.get_id();
100        writer.write_u8(id);
101    }
102
103    pub(crate) fn default_for(ty: AttributeType) -> Self {
104        match ty {
105            AttributeType::Normal => PortabilizationType::OctahedralQuantization,
106            AttributeType::Custom => PortabilizationType::ToBits,
107            _ => PortabilizationType::QuantizationCoordinateWise, // default
108        }
109    }
110}
111
112#[derive(Clone, Copy, Debug)]
113pub struct Config {
114    pub type_: PortabilizationType,
115    pub quantization: Quantization,
116}
117
118impl ConfigType for Config {
119    fn default() -> Self {
120        Config {
121            type_: PortabilizationType::QuantizationCoordinateWise,
122            quantization: Quantization::Bits(11),
123        }
124    }
125}
126
127impl Config {
128    pub fn default_for(ty: AttributeType) -> Self {
129        match ty {
130            AttributeType::Normal => Config {
131                type_: PortabilizationType::OctahedralQuantization,
132                quantization: Quantization::Bits(8),
133            },
134            AttributeType::TextureCoordinate => Config {
135                type_: PortabilizationType::QuantizationCoordinateWise,
136                quantization: Quantization::Bits(10),
137            },
138            AttributeType::Custom => Config {
139                type_: PortabilizationType::ToBits,
140                quantization: Quantization::Bits(11), // not used for ToBits
141            },
142            _ => Self::default(),
143        }
144    }
145}
146
147/// How the quantization resolution (number of bits) for an attribute is
148/// determined. All variants ultimately resolve to a bit count in `1..=30`
149/// (Draco's cap) via [`Quantization::resolve`].
150#[derive(Clone, Copy, Debug, PartialEq)]
151pub enum Quantization {
152    /// Explicit number of quantization bits.
153    Bits(u8),
154    /// Derive the bit count from a maximum tolerated quantization error,
155    /// measured against the attribute's *observed* value range (the largest
156    /// per-axis extent scanned from the data).
157    MaxError(f32),
158    /// Derive the bit count from a maximum tolerated error against a
159    /// *caller-supplied* domain, making the resolution independent of any single
160    /// mesh's extent. `range` is the largest per-axis span of the bounding box
161    /// (see [`Quantization::from_bounding_box`]).
162    Bounded { range: f32, max_error: f32 },
163}
164
165impl Default for Quantization {
166    fn default() -> Self {
167        Quantization::Bits(11)
168    }
169}
170
171impl Quantization {
172    /// Builds a [`Quantization::Bounded`] from an explicit axis-aligned bounding
173    /// box; the largest per-axis span sets the resolution.
174    pub fn from_bounding_box(min: &[f32], max: &[f32], max_error: f32) -> Self {
175        let range = min
176            .iter()
177            .zip(max.iter())
178            .map(|(lo, hi)| hi - lo)
179            .fold(0.0_f32, f32::max);
180        Quantization::Bounded { range, max_error }
181    }
182
183    /// Resolves this spec to a concrete number of quantization bits, clamped to
184    /// `1..=30`. `observed_range` is the largest per-axis extent of the data,
185    /// used only by [`Quantization::MaxError`]; other variants ignore it.
186    pub fn resolve(self, observed_range: f32) -> u8 {
187        let bits = match self {
188            Quantization::Bits(n) => n,
189            Quantization::MaxError(max_error) => bits_for_error(observed_range, max_error),
190            Quantization::Bounded { range, max_error } => bits_for_error(range, max_error),
191        };
192        bits.clamp(1, 30)
193    }
194}
195
196/// Smallest bit count whose quantization step over `range` does not exceed
197/// `max_error`. The decoder dequantizes with step `range / (2^bits - 1)`, so we
198/// need `2^bits >= range / max_error + 1`.
199fn bits_for_error(range: f32, max_error: f32) -> u8 {
200    if range <= 0.0 || max_error <= 0.0 {
201        return 1;
202    }
203    let bits = (range / max_error + 1.0).log2().ceil();
204    bits.clamp(1.0, 30.0) as u8
205}