Skip to main content

box3d_rust/height_field/
types.rs

1//! Height field types and blob serialization.
2//!
3//! Maps C's `b3HeightFieldData` header + trailing arrays (heights, materials, flags).
4//!
5//! SPDX-FileCopyrightText: 2026 Erin Catto
6//! SPDX-License-Identifier: MIT
7
8use crate::math_functions::{Aabb, Vec3, VEC3_ZERO};
9
10/// This material index designates holes in a height field. (B3_HEIGHT_FIELD_HOLE)
11pub const HEIGHT_FIELD_HOLE: u8 = 0xFF;
12
13/// 64-bit height-field version. (B3_HEIGHT_FIELD_VERSION)
14pub const HEIGHT_FIELD_VERSION: u64 = 0x8B18CBD138A6BC84;
15
16/// Size of the C `b3HeightFieldData` header (explicit padding, no unnamed gaps).
17pub const HEIGHT_FIELD_DATA_SIZE: usize = 88;
18
19/// Triangle mesh edge flags. (b3MeshEdgeFlags)
20pub const CONCAVE_EDGE1: i32 = 0x01;
21pub const CONCAVE_EDGE2: i32 = 0x02;
22pub const CONCAVE_EDGE3: i32 = 0x04;
23pub const INVERSE_CONCAVE_EDGE1: i32 = 0x10;
24pub const INVERSE_CONCAVE_EDGE2: i32 = 0x20;
25pub const INVERSE_CONCAVE_EDGE3: i32 = 0x40;
26
27/// Data used to create a height field. (b3HeightFieldDef)
28#[derive(Debug, Clone)]
29pub struct HeightFieldDef {
30    /// Grid point heights; length = count_x * count_z.
31    pub heights: Vec<f32>,
32    /// Grid cell material; length = (count_x - 1) * (count_z - 1).
33    /// A value of [`HEIGHT_FIELD_HOLE`] marks a hole.
34    pub material_indices: Vec<u8>,
35    /// The height field scale. All components must be positive.
36    pub scale: Vec3,
37    /// Number of grid lines along the x-axis.
38    pub count_x: i32,
39    /// Number of grid lines along the z-axis.
40    pub count_z: i32,
41    /// Global minimum height for quantization (unscaled).
42    pub global_minimum_height: f32,
43    /// Global maximum height for quantization (unscaled).
44    pub global_maximum_height: f32,
45    /// Clock-wise winding (inverts the height-field along y).
46    pub clockwise_winding: bool,
47}
48
49impl Default for HeightFieldDef {
50    fn default() -> Self {
51        Self {
52            heights: Vec::new(),
53            material_indices: Vec::new(),
54            scale: VEC3_ZERO,
55            count_x: 0,
56            count_z: 0,
57            global_minimum_height: 0.0,
58            global_maximum_height: 0.0,
59            clockwise_winding: false,
60        }
61    }
62}
63
64/// A height field with compressed storage.
65///
66/// Maps to C's `b3HeightFieldData` + trailing blob. Offsets and `byte_count` match
67/// C so [`HeightFieldData::to_bytes`] reproduces the contiguous layout used by
68/// `b3Hash`.
69#[derive(Debug, Clone)]
70pub struct HeightFieldData {
71    pub version: u64,
72    pub byte_count: i32,
73    pub hash: u32,
74    pub aabb: Aabb,
75    pub min_height: f32,
76    pub max_height: f32,
77    pub height_scale: f32,
78    pub scale: Vec3,
79    pub column_count: i32,
80    pub row_count: i32,
81    pub heights_offset: i32,
82    pub material_offset: i32,
83    pub flags_offset: i32,
84    pub clockwise: bool,
85    pub padding: [u8; 3],
86    pub compressed_heights: Vec<u16>,
87    pub material_indices: Vec<u8>,
88    pub flags: Vec<u8>,
89}
90
91impl Default for HeightFieldData {
92    fn default() -> Self {
93        Self {
94            version: HEIGHT_FIELD_VERSION,
95            byte_count: 0,
96            hash: 0,
97            aabb: Aabb::default(),
98            min_height: 0.0,
99            max_height: 0.0,
100            height_scale: 0.0,
101            scale: VEC3_ZERO,
102            column_count: 0,
103            row_count: 0,
104            heights_offset: 0,
105            material_offset: 0,
106            flags_offset: 0,
107            clockwise: false,
108            padding: [0; 3],
109            compressed_heights: Vec::new(),
110            material_indices: Vec::new(),
111            flags: Vec::new(),
112        }
113    }
114}
115
116/// Compressed height array. (b3GetHeightFieldCompressedHeights)
117pub fn get_height_field_compressed_heights(hf: &HeightFieldData) -> &[u16] {
118    &hf.compressed_heights
119}
120
121/// Material index array. (b3GetHeightFieldMaterialIndices)
122pub fn get_height_field_material_indices(hf: &HeightFieldData) -> &[u8] {
123    &hf.material_indices
124}
125
126/// Triangle flag array. (b3GetHeightFieldFlags)
127pub fn get_height_field_flags(hf: &HeightFieldData) -> &[u8] {
128    &hf.flags
129}
130
131/// Triangle count for a height field. (b3GetHeightFieldTriangleCount)
132pub fn get_height_field_triangle_count(hf: &HeightFieldData) -> i32 {
133    2 * (hf.column_count - 1) * (hf.row_count - 1)
134}
135
136fn write_u64_le(buf: &mut Vec<u8>, v: u64) {
137    buf.extend_from_slice(&v.to_le_bytes());
138}
139
140fn read_u64_le(buf: &[u8], off: usize) -> u64 {
141    u64::from_le_bytes(buf[off..off + 8].try_into().unwrap())
142}
143
144fn read_u32_le(buf: &[u8], off: usize) -> u32 {
145    u32::from_le_bytes(buf[off..off + 4].try_into().unwrap())
146}
147
148fn read_i32_le(buf: &[u8], off: usize) -> i32 {
149    read_u32_le(buf, off) as i32
150}
151
152fn read_u16_le(buf: &[u8], off: usize) -> u16 {
153    u16::from_le_bytes(buf[off..off + 2].try_into().unwrap())
154}
155
156fn read_f32_le(buf: &[u8], off: usize) -> f32 {
157    f32::from_le_bytes(buf[off..off + 4].try_into().unwrap())
158}
159
160fn read_vec3_at(buf: &[u8], off: usize) -> Vec3 {
161    Vec3 {
162        x: read_f32_le(buf, off),
163        y: read_f32_le(buf, off + 4),
164        z: read_f32_le(buf, off + 8),
165    }
166}
167
168fn read_aabb_at(buf: &[u8], off: usize) -> Aabb {
169    Aabb {
170        lower_bound: read_vec3_at(buf, off),
171        upper_bound: read_vec3_at(buf, off + 12),
172    }
173}
174
175/// Restore a height field from a contiguous blob. (inverse of [`HeightFieldData::to_bytes`])
176pub fn convert_bytes_to_height_field(bytes: &[u8]) -> Option<HeightFieldData> {
177    if bytes.len() < HEIGHT_FIELD_DATA_SIZE {
178        return None;
179    }
180    let version = read_u64_le(bytes, 0);
181    if version != HEIGHT_FIELD_VERSION {
182        return None;
183    }
184    let byte_count = read_i32_le(bytes, 8);
185    if byte_count < HEIGHT_FIELD_DATA_SIZE as i32 || bytes.len() != byte_count as usize {
186        return None;
187    }
188    let hash = read_u32_le(bytes, 12);
189    let aabb = read_aabb_at(bytes, 16);
190    let min_height = read_f32_le(bytes, 40);
191    let max_height = read_f32_le(bytes, 44);
192    let height_scale = read_f32_le(bytes, 48);
193    let scale = read_vec3_at(bytes, 52);
194    let column_count = read_i32_le(bytes, 64);
195    let row_count = read_i32_le(bytes, 68);
196    let heights_offset = read_i32_le(bytes, 72);
197    let material_offset = read_i32_le(bytes, 76);
198    let flags_offset = read_i32_le(bytes, 80);
199    let clockwise = bytes[84] != 0;
200    let padding = [bytes[85], bytes[86], bytes[87]];
201
202    if column_count < 0 || row_count < 0 {
203        return None;
204    }
205    let height_count = (column_count as usize).checked_mul(row_count as usize)?;
206    let cell_count =
207        ((column_count - 1).max(0) as usize).checked_mul((row_count - 1).max(0) as usize)?;
208
209    let hoff = heights_offset as usize;
210    if hoff + height_count * 2 > bytes.len() {
211        return None;
212    }
213    let mut compressed_heights = Vec::with_capacity(height_count);
214    for i in 0..height_count {
215        compressed_heights.push(read_u16_le(bytes, hoff + i * 2));
216    }
217
218    let moff = material_offset as usize;
219    if moff + cell_count > bytes.len() {
220        return None;
221    }
222    let material_indices = bytes[moff..moff + cell_count].to_vec();
223
224    let foff = flags_offset as usize;
225    let flag_count = cell_count * 2; // two triangles per cell
226    if foff + flag_count > bytes.len() {
227        return None;
228    }
229    let flags = bytes[foff..foff + flag_count].to_vec();
230
231    Some(HeightFieldData {
232        version,
233        byte_count,
234        hash,
235        aabb,
236        min_height,
237        max_height,
238        height_scale,
239        scale,
240        column_count,
241        row_count,
242        heights_offset,
243        material_offset,
244        flags_offset,
245        clockwise,
246        padding,
247        compressed_heights,
248        material_indices,
249        flags,
250    })
251}
252
253fn write_u32_le(buf: &mut Vec<u8>, v: u32) {
254    buf.extend_from_slice(&v.to_le_bytes());
255}
256
257fn write_i32_le(buf: &mut Vec<u8>, v: i32) {
258    buf.extend_from_slice(&v.to_le_bytes());
259}
260
261fn write_f32_le(buf: &mut Vec<u8>, v: f32) {
262    buf.extend_from_slice(&v.to_le_bytes());
263}
264
265fn write_vec3(buf: &mut Vec<u8>, v: Vec3) {
266    write_f32_le(buf, v.x);
267    write_f32_le(buf, v.y);
268    write_f32_le(buf, v.z);
269}
270
271fn write_aabb(buf: &mut Vec<u8>, a: Aabb) {
272    write_vec3(buf, a.lower_bound);
273    write_vec3(buf, a.upper_bound);
274}
275
276fn pad_to(buf: &mut Vec<u8>, len: usize) {
277    if buf.len() < len {
278        buf.resize(len, 0);
279    }
280}
281
282fn write_header(buf: &mut Vec<u8>, h: &HeightFieldData, hash_override: Option<u32>) {
283    write_u64_le(buf, h.version);
284    write_i32_le(buf, h.byte_count);
285    write_u32_le(buf, hash_override.unwrap_or(h.hash));
286    write_aabb(buf, h.aabb);
287    write_f32_le(buf, h.min_height);
288    write_f32_le(buf, h.max_height);
289    write_f32_le(buf, h.height_scale);
290    write_vec3(buf, h.scale);
291    write_i32_le(buf, h.column_count);
292    write_i32_le(buf, h.row_count);
293    write_i32_le(buf, h.heights_offset);
294    write_i32_le(buf, h.material_offset);
295    write_i32_le(buf, h.flags_offset);
296    buf.push(u8::from(h.clockwise));
297    buf.extend_from_slice(&h.padding);
298    debug_assert_eq!(buf.len(), HEIGHT_FIELD_DATA_SIZE);
299}
300
301impl HeightFieldData {
302    /// Restore from a contiguous blob. (inverse of [`to_bytes`])
303    pub fn from_bytes(bytes: &[u8]) -> Option<HeightFieldData> {
304        convert_bytes_to_height_field(bytes)
305    }
306
307    /// Serialize to the C contiguous trailing-blob layout (for hash parity).
308    pub fn to_bytes(&self) -> Vec<u8> {
309        self.to_bytes_with_hash(self.hash)
310    }
311
312    /// Like [`to_bytes`], but with an explicit hash field (use 0 when computing the hash).
313    pub fn to_bytes_with_hash(&self, hash: u32) -> Vec<u8> {
314        let mut buf = Vec::with_capacity(self.byte_count as usize);
315        write_header(&mut buf, self, Some(hash));
316        pad_to(&mut buf, self.heights_offset as usize);
317        for h in &self.compressed_heights {
318            buf.extend_from_slice(&h.to_le_bytes());
319        }
320        pad_to(&mut buf, self.material_offset as usize);
321        buf.extend_from_slice(&self.material_indices);
322        pad_to(&mut buf, self.flags_offset as usize);
323        buf.extend_from_slice(&self.flags);
324        pad_to(&mut buf, self.byte_count as usize);
325        debug_assert_eq!(buf.len(), self.byte_count as usize);
326        buf
327    }
328}