Skip to main content

plasma_prp/lighting/
mod.rs

1//! Lighting system — plLightInfo, directional/omni/spot lights.
2//!
3//! C++ ref: plGLight/plLightInfo.h/.cpp
4
5use std::io::Read;
6
7use anyhow::Result;
8
9use crate::core::class_index::ClassIndex;
10use crate::core::scene_object::ObjInterfaceData;
11use crate::core::uoid::{Uoid, read_key_uoid};
12use crate::material::layer::Color;
13use crate::resource::prp::PlasmaRead;
14
15/// Light type discriminant.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum LightType {
18    Directional,
19    Omni,
20    Spot,
21    LimitedDir,
22}
23
24/// Parsed light properties common to all light types.
25#[derive(Debug, Clone)]
26pub struct LightProperties {
27    pub ambient: Color,
28    pub diffuse: Color,
29    pub specular: Color,
30    pub soft_volume: Option<Uoid>,
31}
32
33/// Parsed plLightInfo data.
34#[derive(Debug, Clone)]
35pub struct LightInfoData {
36    pub base: ObjInterfaceData,
37    pub light_type: LightType,
38    pub props: LightProperties,
39    /// Light-to-world transform. For directional lights, the Z-axis (m[8..11])
40    /// gives the world-space light direction.
41    pub light_to_world: [f32; 16],
42
43    // Directional has no extra fields
44    // Omni/Spot have:
45    pub attenuation: [f32; 3], // constant, linear, quadratic
46    pub cutoff_distance: f32,
47
48    // Spot only:
49    pub inner_cone: f32,
50    pub outer_cone: f32,
51    pub falloff: f32,
52}
53
54impl LightInfoData {
55    /// Read a plLightInfo (or derived) from a stream.
56    /// The light_type indicates which subclass format to parse.
57    pub fn read(reader: &mut impl Read, light_type: LightType) -> Result<Self> {
58        let base = ObjInterfaceData::read(reader)?;
59
60        // plLightInfo::Read
61        let ambient = Color::read(reader)?;
62        let diffuse = Color::read(reader)?;
63        let specular = Color::read(reader)?;
64
65        // Transform matrices
66        let _ = read_matrix44(reader)?; // light-to-local
67        let _ = read_matrix44(reader)?; // local-to-light
68        let light_to_world = read_matrix44(reader)?;
69        let _ = read_matrix44(reader)?; // world-to-light
70
71        // Reference keys
72        // C++ ref: plLightInfo::Read (plLightInfo.cpp)
73        let _projection = read_key_uoid(reader)?;  // fProjection
74        let soft_volume = read_key_uoid(reader)?;   // fSoftVolume
75        let _scene_node = read_key_uoid(reader)?;   // fSceneNode
76
77        // Visibility regions
78        let num_vis = reader.read_u32()?;
79        for _ in 0..num_vis {
80            let _ = read_key_uoid(reader)?;
81        }
82
83        let mut attenuation = [1.0, 0.0, 0.0];
84        let mut cutoff_distance = 0.0;
85        let mut inner_cone = 0.0;
86        let mut outer_cone = 0.0;
87        let mut falloff = 1.0;
88
89        match light_type {
90            LightType::Omni | LightType::Spot => {
91                // plOmniLightInfo::Read (plLightInfo.cpp:616-621)
92                attenuation[0] = reader.read_f32()?;
93                attenuation[1] = reader.read_f32()?;
94                attenuation[2] = reader.read_f32()?;
95                cutoff_distance = reader.read_f32()?;
96
97                if light_type == LightType::Spot {
98                    // plSpotLightInfo::Read (plLightInfo.cpp:973-979)
99                    // Order: falloff, inner, outer (NOT inner, outer, falloff)
100                    falloff = reader.read_f32()?;
101                    inner_cone = reader.read_f32()?;
102                    outer_cone = reader.read_f32()?;
103                }
104            }
105            LightType::LimitedDir => {
106                // plLimitedDirLightInfo inherits from plDirectionalLightInfo, NOT plOmniLightInfo.
107                // Reads width/height/depth (3 floats), not attenuation (4 floats).
108                // C++ ref: plLightInfo.cpp:658-665
109                let _width = reader.read_f32()?;
110                let _height = reader.read_f32()?;
111                let _depth = reader.read_f32()?;
112            }
113            LightType::Directional => {}
114        }
115
116        Ok(Self {
117            base,
118            light_type,
119            props: LightProperties {
120                ambient,
121                diffuse,
122                specular,
123                soft_volume,
124            },
125            light_to_world,
126            attenuation,
127            cutoff_distance,
128            inner_cone,
129            outer_cone,
130            falloff,
131        })
132    }
133}
134
135fn read_matrix44(reader: &mut impl Read) -> Result<[f32; 16]> {
136    let flag = reader.read_u8()?;
137    if flag == 0 {
138        return Ok([
139            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
140        ]);
141    }
142    let mut m = [0f32; 16];
143    for val in &mut m {
144        *val = reader.read_f32()?;
145    }
146    Ok(m)
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::core::class_index::ClassIndex;
153    use crate::resource::prp::PrpPage;
154    use std::io::Cursor;
155    use std::path::Path;
156
157    #[test]
158    fn test_parse_cleft_lights() {
159        let path = Path::new("../../Plasma/staging/client/dat/Cleft_District_Cleft.prp");
160        if !path.exists() {
161            eprintln!("Skipping test: {:?} not found", path);
162            return;
163        }
164
165        let page = PrpPage::from_file(path).unwrap();
166        let mut parsed = 0;
167
168        // Try directional lights
169        for key in page.keys_of_type(ClassIndex::PL_DIRECTIONAL_LIGHT_INFO) {
170            if let Some(data) = page.object_data(key) {
171                let mut cursor = Cursor::new(data);
172                let _ = cursor.read_i16().unwrap();
173
174                match LightInfoData::read(&mut cursor, LightType::Directional) {
175                    Ok(light) => {
176                        parsed += 1;
177                        eprintln!(
178                            "  DirectionalLight '{}': diffuse=({:.2},{:.2},{:.2})",
179                            key.object_name,
180                            light.props.diffuse.r,
181                            light.props.diffuse.g,
182                            light.props.diffuse.b,
183                        );
184                    }
185                    Err(e) => {
186                        eprintln!("Failed to parse directional light '{}': {}", key.object_name, e);
187                    }
188                }
189            }
190        }
191
192        // Try omni lights
193        for key in page.keys_of_type(ClassIndex::PL_OMNI_LIGHT_INFO) {
194            if let Some(data) = page.object_data(key) {
195                let mut cursor = Cursor::new(data);
196                let _ = cursor.read_i16().unwrap();
197
198                match LightInfoData::read(&mut cursor, LightType::Omni) {
199                    Ok(_) => parsed += 1,
200                    Err(e) => {
201                        eprintln!("Failed to parse omni light '{}': {}", key.object_name, e);
202                    }
203                }
204            }
205        }
206
207        eprintln!("Parsed {} lights from Cleft", parsed);
208        // Cleft_District_Cleft.prp has 6 directional + 11 omni lights
209        assert!(parsed >= 11, "Cleft should have at least 11 lights, got {}", parsed);
210    }
211}