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, ComponentDataType};
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        };
52        debug_write!("End of Portabilization Metadata", writer);
53        out
54    }
55
56    pub fn portabilize(self) -> Attribute {
57        match self {
58            Portabilization::QuantizationCoordinateWise(qcw) => qcw.portabilize(),
59            Portabilization::OctahedralQuantization(oct) => oct.portabilize(),
60            Portabilization::ToBits(tb) => tb.portabilize(),
61        }
62    }
63}
64
65pub trait PortabilizationImpl<const N: usize>
66where
67    NdVector<N, i32>: Vector<N, Component = i32>,
68{
69    /// portabilizes the whole data.
70    fn portabilize(self) -> Attribute;
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum PortabilizationType {
75    QuantizationCoordinateWise,
76    OctahedralQuantization,
77    ToBits,
78}
79
80impl PortabilizationType {
81    pub(crate) fn get_id(&self) -> u8 {
82        match self {
83            PortabilizationType::ToBits => 1,
84            PortabilizationType::QuantizationCoordinateWise => 2,
85            PortabilizationType::OctahedralQuantization => 3,
86        }
87    }
88
89    pub(crate) fn write_to<W>(&self, writer: &mut W)
90    where
91        W: ByteWriter,
92    {
93        let id = self.get_id();
94        writer.write_u8(id);
95    }
96
97    /// The default portabilization for an attribute of type `ty` with
98    /// components of `component_ty`. Integer values ride the integer codec
99    /// whatever the attribute type; float quantization is only valid for
100    /// float input (the reference decoder rejects a quantization block on a
101    /// non-float declared type).
102    pub(crate) fn default_for(ty: AttributeType, component_ty: ComponentDataType) -> Self {
103        if component_ty.is_integer() {
104            return PortabilizationType::ToBits;
105        }
106        match ty {
107            AttributeType::Normal => PortabilizationType::OctahedralQuantization,
108            // Float values of every other type, generics included, are
109            // quantized: `ToBits` truncates floats numerically, and the
110            // reference's lossless float form (the raw type 0 codec) is not
111            // implemented.
112            _ => PortabilizationType::QuantizationCoordinateWise,
113        }
114    }
115}
116
117#[derive(Clone, Copy, Debug)]
118pub struct Config {
119    pub type_: PortabilizationType,
120    pub quantization: Quantization,
121}
122
123impl ConfigType for Config {
124    fn default() -> Self {
125        Config {
126            type_: PortabilizationType::QuantizationCoordinateWise,
127            quantization: Quantization::Bits(11),
128        }
129    }
130}
131
132impl Config {
133    /// The octahedral lattice this config quantizes onto, or `0` if it
134    /// portabilizes some other way.
135    pub fn oct_center(&self) -> i32 {
136        match self.type_ {
137            PortabilizationType::OctahedralQuantization => {
138                draco_oxide_core::codec::attribute::geom::oct_center(self.quantization.resolve(0.0))
139            }
140            _ => 0,
141        }
142    }
143
144    pub fn default_for(ty: AttributeType, component_ty: ComponentDataType) -> Self {
145        if component_ty.is_integer() {
146            return Config {
147                type_: PortabilizationType::ToBits,
148                quantization: Quantization::Bits(11), // not used for ToBits
149            };
150        }
151        match ty {
152            AttributeType::Normal => Config {
153                type_: PortabilizationType::OctahedralQuantization,
154                quantization: Quantization::Bits(8),
155            },
156            AttributeType::TextureCoordinate => Config {
157                type_: PortabilizationType::QuantizationCoordinateWise,
158                quantization: Quantization::Bits(10),
159            },
160            _ => Self::default(),
161        }
162    }
163}
164
165/// How the quantization resolution (number of bits) for an attribute is
166/// determined. All variants ultimately resolve to a bit count in `1..=30`
167/// (Draco's cap) via [`Quantization::resolve`].
168#[derive(Clone, Copy, Debug, PartialEq)]
169pub enum Quantization {
170    /// Explicit number of quantization bits.
171    Bits(u8),
172    /// Derive the bit count from a maximum tolerated quantization error,
173    /// measured against the attribute's *observed* value range (the largest
174    /// per-axis extent scanned from the data).
175    MaxError(f32),
176    /// Derive the bit count from a maximum tolerated error against a
177    /// *caller-supplied* domain, making the resolution independent of any single
178    /// mesh's extent. `range` is the largest per-axis span of the bounding box
179    /// (see [`Quantization::from_bounding_box`]).
180    Bounded { range: f32, max_error: f32 },
181}
182
183impl Default for Quantization {
184    fn default() -> Self {
185        Quantization::Bits(11)
186    }
187}
188
189impl Quantization {
190    /// Builds a [`Quantization::Bounded`] from an explicit axis-aligned bounding
191    /// box; the largest per-axis span sets the resolution.
192    pub fn from_bounding_box(min: &[f32], max: &[f32], max_error: f32) -> Self {
193        let range = min
194            .iter()
195            .zip(max.iter())
196            .map(|(lo, hi)| hi - lo)
197            .fold(0.0_f32, f32::max);
198        Quantization::Bounded { range, max_error }
199    }
200
201    /// Resolves this spec to a concrete number of quantization bits, clamped to
202    /// `1..=30`. `observed_range` is the largest per-axis extent of the data,
203    /// used only by [`Quantization::MaxError`]; other variants ignore it.
204    pub fn resolve(self, observed_range: f32) -> u8 {
205        let bits = match self {
206            Quantization::Bits(n) => n,
207            Quantization::MaxError(max_error) => bits_for_error(observed_range, max_error),
208            Quantization::Bounded { range, max_error } => bits_for_error(range, max_error),
209        };
210        bits.clamp(1, 30)
211    }
212}
213
214/// Smallest bit count whose quantization step over `range` does not exceed
215/// `max_error`. The decoder dequantizes with step `range / (2^bits - 1)`, so we
216/// need `2^bits >= range / max_error + 1`.
217fn bits_for_error(range: f32, max_error: f32) -> u8 {
218    if range <= 0.0 || max_error <= 0.0 {
219        return 1;
220    }
221    let bits = (range / max_error + 1.0).log2().ceil();
222    bits.clamp(1.0, 30.0) as u8
223}