Skip to main content

plasma_prp/resource/
cluster.rs

1//! plClusterGroup parser — extracts instanced vegetation geometry from .prp data
2//!
3//! C++ ref: plClusterGroup.cpp, plCluster.cpp, plSpanTemplate.cpp, plSpanInstance.cpp
4//! plClusterGroup contains a shared vertex template (plSpanTemplate) that is stamped
5//! at many world positions via plSpanInstance transforms. At runtime, C++ unpacks these
6//! into plDrawableSpans with kLiteVtxNonPreshaded | kPropRunTimeLight.
7
8use anyhow::{Result, bail};
9use std::io::{Read, Seek};
10use crate::resource::prp::PlasmaRead;
11use crate::core::uoid::{Uoid, read_key_uoid};
12
13/// Vertex produced by cluster unpacking — position, normal, color, UVs, flags.
14/// Mirrors the engine renderer's Vertex layout so callers can transmute or convert.
15#[derive(Debug, Clone, Copy)]
16#[repr(C)]
17pub struct ClusterVertex {
18    pub position: [f32; 4],
19    pub normal: [f32; 4],
20    pub color: [f32; 4],
21    pub uv: [f32; 2],
22    pub uv2: [f32; 2],
23    pub uv3: [f32; 2],
24    pub flags: f32,
25    pub _pad: f32,
26    pub uv4: [f32; 2],
27    pub uv5: [f32; 2],
28}
29
30/// Parsed cluster group — template geometry + per-instance transforms
31#[derive(Debug)]
32pub struct ClusterGroup {
33    pub template: SpanTemplate,
34    pub material_uoid: Option<Uoid>,
35    pub clusters: Vec<Cluster>,
36}
37
38/// Shared vertex/index template for all instances in a cluster group
39#[derive(Debug)]
40pub struct SpanTemplate {
41    pub format: u16,
42    pub num_verts: u16,
43    pub num_tris: u16,
44    pub stride: usize,
45    pub vert_data: Vec<u8>,
46    pub index_data: Vec<u16>,
47}
48
49/// A cluster contains multiple span instances sharing the same template
50#[derive(Debug)]
51pub struct Cluster {
52    pub encoding: SpanEncoding,
53    pub instances: Vec<SpanInstance>,
54}
55
56/// Encoding descriptor for per-instance position deltas and colors
57#[derive(Debug, Clone, Copy)]
58pub struct SpanEncoding {
59    pub code: u8,
60    pub pos_scale: f32,
61}
62
63/// A single instance: 3x4 transform matrix + optional position deltas + optional colors
64#[derive(Debug)]
65pub struct SpanInstance {
66    pub l2w: [[f32; 4]; 3],
67    pub pos_delta: Option<Vec<u8>>,
68    pub col: Option<Vec<u8>>,
69}
70
71// SpanEncoding position format flags
72const POS_NONE: u8 = 0x0;
73const POS_888: u8 = 0x1;
74const POS_161616: u8 = 0x2;
75const POS_101010: u8 = 0x4;
76const POS_008: u8 = 0x8;
77const POS_MASK: u8 = POS_888 | POS_161616 | POS_101010 | POS_008;
78
79// SpanEncoding color format flags
80const COL_NONE: u16 = 0x0;
81const COL_A8: u16 = 0x10;
82const COL_I8: u16 = 0x20;
83const COL_AI88: u16 = 0x40;
84const COL_RGB888: u16 = 0x80;
85const COL_ARGB8888: u16 = 0x100;
86const COL_MASK: u16 = COL_A8 | COL_I8 | COL_AI88 | COL_RGB888 | COL_ARGB8888;
87
88// SpanTemplate format flags
89const TMPL_POS: u16 = 0x1;
90const TMPL_NORM: u16 = 0x2;
91const TMPL_COLOR: u16 = 0x4;
92const TMPL_WGTIDX: u16 = 0x8;
93const TMPL_UVW_MASK: u16 = 0xF0;
94const TMPL_WEIGHT_MASK: u16 = 0x300;
95const TMPL_COLOR2: u16 = 0x400;
96
97impl SpanEncoding {
98    fn pos_stride(&self) -> usize {
99        match self.code & POS_MASK {
100            POS_888 => 3,
101            POS_161616 => 6,
102            POS_101010 => 4,
103            POS_008 => 1,
104            _ => 0,
105        }
106    }
107
108    fn col_stride(&self) -> usize {
109        let code = self.code as u16;
110        match code & COL_MASK {
111            COL_A8 | COL_I8 => 1,
112            COL_AI88 => 2,
113            COL_RGB888 => 3,
114            COL_ARGB8888 => 4,
115            _ => 0,
116        }
117    }
118}
119
120impl SpanTemplate {
121    pub fn num_uvws(&self) -> usize { ((self.format & TMPL_UVW_MASK) >> 4) as usize }
122    fn num_weights(&self) -> usize { ((self.format & TMPL_WEIGHT_MASK) >> 8) as usize }
123    fn has_pos(&self) -> bool { self.format & TMPL_POS != 0 }
124    fn has_norm(&self) -> bool { self.format & TMPL_NORM != 0 }
125    fn has_color(&self) -> bool { self.format & TMPL_COLOR != 0 }
126    fn has_color2(&self) -> bool { self.format & TMPL_COLOR2 != 0 }
127    fn has_wgt_idx(&self) -> bool { self.format & TMPL_WGTIDX != 0 }
128
129    fn calc_stride(format: u16) -> usize {
130        let mut s = 0usize;
131        if format & TMPL_POS != 0 { s += 12; } // hsPoint3
132        let num_weights = ((format & TMPL_WEIGHT_MASK) >> 8) as usize;
133        s += num_weights * 4; // float per weight
134        if format & TMPL_WGTIDX != 0 { s += 4; } // uint32
135        if format & TMPL_NORM != 0 { s += 12; } // hsVector3
136        if format & TMPL_COLOR != 0 { s += 4; } // uint32
137        if format & TMPL_COLOR2 != 0 { s += 4; } // uint32
138        let num_uvws = ((format & TMPL_UVW_MASK) >> 4) as usize;
139        s += num_uvws * 12; // hsPoint3 per UVW
140        s
141    }
142
143    // Offsets into per-vertex data
144    fn pos_offset(&self) -> usize { 0 }
145    fn norm_offset(&self) -> usize {
146        let mut o = if self.has_pos() { 12 } else { 0 };
147        o += self.num_weights() * 4;
148        if self.has_wgt_idx() { o += 4; }
149        o
150    }
151    fn color_offset(&self) -> usize {
152        self.norm_offset() + if self.has_norm() { 12 } else { 0 }
153    }
154    fn color2_offset(&self) -> usize {
155        self.color_offset() + if self.has_color() { 4 } else { 0 }
156    }
157    fn uvw_offset(&self) -> usize {
158        self.color2_offset() + if self.has_color2() { 4 } else { 0 }
159    }
160}
161
162impl ClusterGroup {
163    /// Parse a plClusterGroup from object data.
164    /// C++ ref: plClusterGroup::Read (plClusterGroup.cpp:103-136)
165    pub fn read(reader: &mut (impl Read + Seek)) -> Result<Self> {
166        // hsKeyedObject::Read — read self-key
167        let _self_key = read_key_uoid(reader)?;
168
169        // Read plSpanTemplate
170        let template = SpanTemplate::read(reader)?;
171
172        // Read material key ref (mgr->ReadKeyNotifyMe)
173        let material_uoid = read_key_uoid(reader)?;
174
175        // Read clusters
176        let num_clusters = reader.read_u32()? as usize;
177        let mut clusters = Vec::with_capacity(num_clusters);
178        for _ in 0..num_clusters {
179            clusters.push(Cluster::read(reader, template.num_verts)?);
180        }
181
182        // Skip vis regions (key refs)
183        let num_regions = reader.read_u32()? as usize;
184        for _ in 0..num_regions {
185            let _ = read_key_uoid(reader)?;
186        }
187
188        // Skip light refs (key refs)
189        let num_lights = reader.read_u32()? as usize;
190        for _ in 0..num_lights {
191            let _ = read_key_uoid(reader)?;
192        }
193
194        // LOD distances
195        let _min_dist = reader.read_f32()?;
196        let _max_dist = reader.read_f32()?;
197
198        // Render level
199        let _render_level = reader.read_u32()?;
200
201        // Scene node key
202        let _ = read_key_uoid(reader)?;
203
204        let total_insts: usize = clusters.iter().map(|c| c.instances.len()).sum();
205        log::debug!("ClusterGroup: {} clusters, {} total instances, template: {} verts, {} tris, format=0x{:X}",
206            clusters.len(), total_insts, template.num_verts, template.num_tris, template.format);
207
208        Ok(Self {
209            template,
210            material_uoid,
211            clusters,
212        })
213    }
214
215    /// Unpack all instances into individual meshes (vertices + indices).
216    /// Each cluster becomes one mesh with all its instances merged.
217    /// C++ ref: plCluster::UnPack (plCluster.cpp:112-207)
218    pub fn unpack_meshes(&self) -> Vec<(Vec<ClusterVertex>, Vec<u16>)> {
219        let templ = &self.template;
220        let verts_per_inst = templ.num_verts as usize;
221        let indices_per_inst = templ.num_tris as usize * 3;
222
223        let mut result = Vec::new();
224
225        for cluster in &self.clusters {
226            let num_insts = cluster.instances.len();
227            if num_insts == 0 { continue; }
228
229            let mut all_verts = Vec::with_capacity(verts_per_inst * num_insts);
230            let mut all_indices = Vec::with_capacity(indices_per_inst * num_insts);
231            let mut idx_offset = 0u16;
232
233            for inst in &cluster.instances {
234                // Copy template indices with offset
235                for &idx in &templ.index_data {
236                    all_indices.push(idx + idx_offset);
237                }
238                idx_offset += verts_per_inst as u16;
239
240                // Build local-to-world matrix (3x4 → 4x4 row-major flat)
241                let l2w = &inst.l2w;
242                let m: [f32; 16] = [
243                    l2w[0][0], l2w[0][1], l2w[0][2], l2w[0][3],
244                    l2w[1][0], l2w[1][1], l2w[1][2], l2w[1][3],
245                    l2w[2][0], l2w[2][1], l2w[2][2], l2w[2][3],
246                    0.0, 0.0, 0.0, 1.0,
247                ];
248
249                // Compute inverse-transpose for normal transform
250                // For a 3x3 rotation+scale, transpose of inverse ≈ cofactor matrix
251                let w2l_t = transpose_inverse_3x3(&m);
252
253                // Create position delta iterator
254                let enc = &cluster.encoding;
255                let pos_stride = enc.pos_stride();
256                let col_stride = enc.col_stride();
257
258                for vi in 0..verts_per_inst {
259                    let base = vi * templ.stride;
260
261                    // Read position from template
262                    let pos_off = templ.pos_offset();
263                    let (mut px, mut py, mut pz) = if templ.has_pos() && base + pos_off + 12 <= templ.vert_data.len() {
264                        let o = base + pos_off;
265                        (
266                            f32::from_le_bytes(templ.vert_data[o..o+4].try_into().unwrap()),
267                            f32::from_le_bytes(templ.vert_data[o+4..o+8].try_into().unwrap()),
268                            f32::from_le_bytes(templ.vert_data[o+8..o+12].try_into().unwrap()),
269                        )
270                    } else {
271                        (0.0, 0.0, 0.0)
272                    };
273
274                    // Apply position delta if present
275                    if let Some(ref pos_data) = inst.pos_delta {
276                        let delta_off = vi * pos_stride;
277                        let (dx, dy, dz) = decode_pos_delta(enc, pos_data, delta_off);
278                        px += dx;
279                        py += dy;
280                        pz += dz;
281                    }
282
283                    // Transform position by L2W
284                    let wx = px * m[0] + py * m[1] + pz * m[2] + m[3];
285                    let wy = px * m[4] + py * m[5] + pz * m[6] + m[7];
286                    let wz = px * m[8] + py * m[9] + pz * m[10] + m[11];
287
288                    // Read normal from template
289                    let norm_off = templ.norm_offset();
290                    let (nx, ny, nz) = if templ.has_norm() && base + norm_off + 12 <= templ.vert_data.len() {
291                        let o = base + norm_off;
292                        (
293                            f32::from_le_bytes(templ.vert_data[o..o+4].try_into().unwrap()),
294                            f32::from_le_bytes(templ.vert_data[o+4..o+8].try_into().unwrap()),
295                            f32::from_le_bytes(templ.vert_data[o+8..o+12].try_into().unwrap()),
296                        )
297                    } else {
298                        (0.0, 1.0, 0.0)
299                    };
300
301                    // Transform normal by inverse-transpose
302                    let wnx = nx * w2l_t[0] + ny * w2l_t[1] + nz * w2l_t[2];
303                    let wny = nx * w2l_t[3] + ny * w2l_t[4] + nz * w2l_t[5];
304                    let wnz = nx * w2l_t[6] + ny * w2l_t[7] + nz * w2l_t[8];
305                    let nlen = (wnx*wnx + wny*wny + wnz*wnz).sqrt().max(1e-10);
306
307                    // Read color from template, apply instance color override
308                    let (r, g, b, a) = if templ.has_color() {
309                        let col_off = templ.color_offset();
310                        let o = base + col_off;
311                        if o + 4 <= templ.vert_data.len() {
312                            let mut c = u32::from_le_bytes(templ.vert_data[o..o+4].try_into().unwrap());
313                            // Apply instance color override
314                            if let Some(ref col_data) = inst.col {
315                                c = decode_color(enc, col_data, vi * col_stride, c);
316                            }
317                            // D3DCOLOR format: 0xAARRGGBB (LE bytes: B,G,R,A)
318                            let ca = ((c >> 24) & 0xFF) as f32 / 255.0;
319                            let cr = ((c >> 16) & 0xFF) as f32 / 255.0;
320                            let cg = ((c >> 8) & 0xFF) as f32 / 255.0;
321                            let cb = (c & 0xFF) as f32 / 255.0;
322                            (cr, cg, cb, ca)
323                        } else {
324                            (1.0, 1.0, 1.0, 1.0)
325                        }
326                    } else {
327                        (1.0, 1.0, 1.0, 1.0)
328                    };
329
330                    // Read UV0 from template
331                    let uvw_off = templ.uvw_offset();
332                    let (u, v) = if templ.num_uvws() > 0 {
333                        let o = base + uvw_off;
334                        if o + 8 <= templ.vert_data.len() {
335                            (
336                                f32::from_le_bytes(templ.vert_data[o..o+4].try_into().unwrap()),
337                                f32::from_le_bytes(templ.vert_data[o+4..o+8].try_into().unwrap()),
338                            )
339                        } else {
340                            (0.0, 0.0)
341                        }
342                    } else {
343                        (0.0, 0.0)
344                    };
345
346                    // Read UV1 from template
347                    let (u2, v2) = if templ.num_uvws() > 1 {
348                        let o = base + uvw_off + 12; // skip UV0 (hsPoint3 = 12 bytes)
349                        if o + 8 <= templ.vert_data.len() {
350                            (
351                                f32::from_le_bytes(templ.vert_data[o..o+4].try_into().unwrap()),
352                                f32::from_le_bytes(templ.vert_data[o+4..o+8].try_into().unwrap()),
353                            )
354                        } else {
355                            (0.0, 0.0)
356                        }
357                    } else {
358                        (0.0, 0.0)
359                    };
360
361                    all_verts.push(ClusterVertex {
362                        position: [wx, wy, wz, 0.0],
363                        normal: [wnx / nlen, wny / nlen, wnz / nlen, 0.0],
364                        color: [r, g, b, a],
365                        uv: [u, v],
366                        uv2: [u2, v2],
367                        uv3: [0.0, 0.0],
368                        // kLiteVtxNonPreshaded flag = bit 1 (2.0)
369                        flags: 2.0,
370                        _pad: 0.0,
371                        uv4: [0.0, 0.0],
372                        uv5: [0.0, 0.0],
373                    });
374                }
375            }
376
377            if !all_verts.is_empty() && !all_indices.is_empty() {
378                result.push((all_verts, all_indices));
379            }
380        }
381
382        result
383    }
384}
385
386impl SpanTemplate {
387    fn read(reader: &mut (impl Read + Seek)) -> Result<Self> {
388        let num_verts = reader.read_u16()?;
389        let format = reader.read_u16()?;
390        let num_tris = reader.read_u16()?;
391
392        let stride = Self::calc_stride(format);
393        let vert_size = num_verts as usize * stride;
394        let idx_size = num_tris as usize * 3;
395
396        let mut vert_data = vec![0u8; vert_size];
397        reader.read_exact(&mut vert_data)?;
398
399        let mut index_data = Vec::with_capacity(idx_size);
400        for _ in 0..idx_size {
401            index_data.push(reader.read_u16()?);
402        }
403
404        Ok(Self { format, num_verts, num_tris, stride, vert_data, index_data })
405    }
406}
407
408impl Cluster {
409    fn read(reader: &mut (impl Read + Seek), num_template_verts: u16) -> Result<Self> {
410        // plSpanEncoding
411        let code = reader.read_u8()?;
412        let pos_scale = reader.read_f32()?;
413        let encoding = SpanEncoding { code, pos_scale };
414
415        let num_verts = num_template_verts as usize;
416        let num_insts = reader.read_u32()? as usize;
417        let mut instances = Vec::with_capacity(num_insts);
418
419        let pos_stride = encoding.pos_stride();
420        let col_stride = encoding.col_stride();
421
422        for _ in 0..num_insts {
423            // 3x4 transform matrix (12 floats)
424            let mut l2w = [[0.0f32; 4]; 3];
425            for row in &mut l2w {
426                for col in row.iter_mut() {
427                    *col = reader.read_f32()?;
428                }
429            }
430
431            // Position deltas (optional, based on encoding)
432            let pos_delta = if pos_stride > 0 {
433                let size = num_verts * pos_stride;
434                let mut data = vec![0u8; size];
435                reader.read_exact(&mut data)?;
436                Some(data)
437            } else {
438                None
439            };
440
441            // Color data (optional, based on encoding)
442            let col = if col_stride > 0 {
443                let size = num_verts * col_stride;
444                let mut data = vec![0u8; size];
445                reader.read_exact(&mut data)?;
446                Some(data)
447            } else {
448                None
449            };
450
451            instances.push(SpanInstance { l2w, pos_delta, col });
452        }
453
454        Ok(Self { encoding, instances })
455    }
456}
457
458/// Decode position delta from encoded data.
459/// C++ ref: plSpanInstanceIter::DelPos (plSpanInstance.h:246-266)
460fn decode_pos_delta(enc: &SpanEncoding, data: &[u8], offset: usize) -> (f32, f32, f32) {
461    match enc.code & POS_MASK {
462        POS_888 => {
463            if offset + 3 <= data.len() {
464                let dx = data[offset] as i8 as f32 * enc.pos_scale;
465                let dy = data[offset + 1] as i8 as f32 * enc.pos_scale;
466                let dz = data[offset + 2] as i8 as f32 * enc.pos_scale;
467                (dx, dy, dz)
468            } else { (0.0, 0.0, 0.0) }
469        }
470        POS_161616 => {
471            if offset + 6 <= data.len() {
472                let dx = i16::from_le_bytes(data[offset..offset+2].try_into().unwrap()) as f32 * enc.pos_scale;
473                let dy = i16::from_le_bytes(data[offset+2..offset+4].try_into().unwrap()) as f32 * enc.pos_scale;
474                let dz = i16::from_le_bytes(data[offset+4..offset+6].try_into().unwrap()) as f32 * enc.pos_scale;
475                (dx, dy, dz)
476            } else { (0.0, 0.0, 0.0) }
477        }
478        POS_101010 => {
479            if offset + 4 <= data.len() {
480                let packed = u32::from_le_bytes(data[offset..offset+4].try_into().unwrap());
481                let dx = (packed & 0x3F) as f32 * enc.pos_scale;
482                let dy = ((packed >> 10) & 0x3F) as f32 * enc.pos_scale;
483                let dz = ((packed >> 20) & 0x3F) as f32 * enc.pos_scale;
484                (dx, dy, dz)
485            } else { (0.0, 0.0, 0.0) }
486        }
487        POS_008 => {
488            if offset < data.len() {
489                let dz = data[offset] as i8 as f32 * enc.pos_scale;
490                (0.0, 0.0, dz)
491            } else { (0.0, 0.0, 0.0) }
492        }
493        _ => (0.0, 0.0, 0.0),
494    }
495}
496
497/// Decode color from encoded data, merging with template color.
498/// C++ ref: plSpanInstanceIter::Color (plSpanInstance.h:271-299)
499fn decode_color(enc: &SpanEncoding, data: &[u8], offset: usize, template_color: u32) -> u32 {
500    let code = enc.code as u16;
501    match code & COL_MASK {
502        COL_A8 => {
503            if offset < data.len() {
504                (template_color & 0x00FFFFFF) | ((data[offset] as u32) << 24)
505            } else { template_color }
506        }
507        COL_I8 => {
508            if offset < data.len() {
509                let v = data[offset] as u32;
510                (template_color & 0xFF000000) | (v << 16) | (v << 8) | v
511            } else { template_color }
512        }
513        COL_AI88 => {
514            if offset + 2 <= data.len() {
515                let val = u16::from_le_bytes(data[offset..offset+2].try_into().unwrap());
516                let col = (val & 0xFF) as u32;
517                let alpha = ((val & 0xFF00) >> 8) as u32;
518                (alpha << 24) | (col << 16) | (col << 8) | col
519            } else { template_color }
520        }
521        COL_RGB888 => {
522            if offset + 3 <= data.len() {
523                (template_color & 0xFF000000)
524                    | ((data[offset] as u32) << 16)
525                    | ((data[offset + 1] as u32) << 8)
526                    | (data[offset + 2] as u32)
527            } else { template_color }
528        }
529        COL_ARGB8888 => {
530            if offset + 4 <= data.len() {
531                u32::from_le_bytes(data[offset..offset+4].try_into().unwrap())
532            } else { template_color }
533        }
534        _ => template_color,
535    }
536}
537
538/// Compute 3x3 inverse-transpose for normal transformation.
539/// Returns [row0_x, row0_y, row0_z, row1_x, row1_y, row1_z, row2_x, row2_y, row2_z]
540fn transpose_inverse_3x3(m: &[f32; 16]) -> [f32; 9] {
541    // Extract 3x3 rotation/scale from row-major 4x4
542    let a = m[0]; let b = m[1]; let c = m[2];
543    let d = m[4]; let e = m[5]; let f = m[6];
544    let g = m[8]; let h = m[9]; let i = m[10];
545
546    let det = a*(e*i - f*h) - b*(d*i - f*g) + c*(d*h - e*g);
547    if det.abs() < 1e-10 {
548        return [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
549    }
550    let inv_det = 1.0 / det;
551
552    // Cofactor matrix (which is the transpose of the inverse × det)
553    // Then transpose that to get inverse-transpose
554    // inverse-transpose = cofactor / det ... but we want cofactor^T / det
555    // Actually: (M^-1)^T = cofactor(M) / det
556    // So inverse-transpose row i, col j = cofactor(i,j) / det
557    [
558        (e*i - f*h) * inv_det, (f*g - d*i) * inv_det, (d*h - e*g) * inv_det,
559        (c*h - b*i) * inv_det, (a*i - c*g) * inv_det, (b*g - a*h) * inv_det,
560        (b*f - c*e) * inv_det, (c*d - a*f) * inv_det, (a*e - b*d) * inv_det,
561    ]
562}