Skip to main content

siglus_scene_vm/runtime/
tonecurve.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use anyhow::Result;
5use siglus_assets::gameexe::{decode_gameexe_dat_bytes, GameexeConfig, GameexeDecodeOptions};
6
7use crate::assets::RgbaImage;
8use crate::image_manager::{ImageId, ImageManager};
9
10#[derive(Debug, Clone)]
11struct ToneCurveRow {
12    lut_r: [u8; 256],
13    lut_g: [u8; 256],
14    lut_b: [u8; 256],
15    sat: i32,
16}
17
18#[derive(Debug, Default)]
19pub struct ToneCurveRuntime {
20    rows: Option<Vec<Option<ToneCurveRow>>>,
21    cache: HashMap<(ImageId, u64, i32), ImageId>,
22    source_path: Option<PathBuf>,
23    lut_image_id: Option<ImageId>,
24}
25
26impl ToneCurveRuntime {
27    pub fn new(project_dir: &Path) -> Self {
28        let mut out = Self::default();
29        if let Some(path) = find_tonecurve_path(project_dir) {
30            out.source_path = Some(path.clone());
31            out.rows = load_tonecurve_rows(&path).ok();
32        }
33        out
34    }
35
36    pub fn has_table(&self) -> bool {
37        self.rows.is_some()
38    }
39
40    pub fn shader_binding(
41        &mut self,
42        images: &mut ImageManager,
43        tonecurve_no: i32,
44    ) -> Option<(ImageId, f32, f32)> {
45        let idx = tonecurve_no.max(0) as usize;
46        let row = self.rows.as_ref()?.get(idx)?.as_ref()?.clone();
47        let lut_id = self.ensure_lut_image(images)?;
48        let row_y = ((idx.min(255) as f32) + 0.5) / 256.0;
49        let sat = if row.sat < 0 {
50            ((-row.sat) as f32 / 100.0).clamp(0.0, 1.0)
51        } else {
52            0.0
53        };
54        Some((lut_id, row_y, sat))
55    }
56
57    fn ensure_lut_image(&mut self, images: &mut ImageManager) -> Option<ImageId> {
58        if let Some(id) = self.lut_image_id {
59            return Some(id);
60        }
61        let rows = self.rows.as_ref()?;
62        let mut rgba = vec![0u8; 256 * 256 * 4];
63        for y in 0..256usize {
64            for x in 0..256usize {
65                let idx = (y * 256 + x) * 4;
66                if let Some(Some(row)) = rows.get(y) {
67                    rgba[idx] = row.lut_r[x];
68                    rgba[idx + 1] = row.lut_g[x];
69                    rgba[idx + 2] = row.lut_b[x];
70                } else {
71                    let v = x as u8;
72                    rgba[idx] = v;
73                    rgba[idx + 1] = v;
74                    rgba[idx + 2] = v;
75                }
76                rgba[idx + 3] = 255;
77            }
78        }
79        let id = images.insert_image(RgbaImage {
80            width: 256,
81            height: 256,
82            center_x: 0,
83            center_y: 0,
84            rgba,
85        });
86        self.lut_image_id = Some(id);
87        Some(id)
88    }
89
90    pub fn apply_cached(
91        &mut self,
92        images: &mut ImageManager,
93        base_id: ImageId,
94        tonecurve_no: i32,
95    ) -> Option<ImageId> {
96        let rows = self.rows.as_ref()?;
97        let row = rows.get(tonecurve_no.max(0) as usize)?.as_ref()?;
98        let (base, version) = images.get_entry(base_id)?;
99        if let Some(id) = self.cache.get(&(base_id, version, tonecurve_no)).copied() {
100            return Some(id);
101        }
102        let toned = apply_tonecurve_to_image(base, row);
103        let toned_id = images.insert_image(toned);
104        self.cache
105            .insert((base_id, version, tonecurve_no), toned_id);
106        Some(toned_id)
107    }
108}
109
110fn apply_tonecurve_to_image(src: &RgbaImage, row: &ToneCurveRow) -> RgbaImage {
111    let mut out = src.clone();
112    let mono_amt = if row.sat < 0 {
113        ((-row.sat) as f32 / 100.0).clamp(0.0, 1.0)
114    } else {
115        0.0
116    };
117    for px in out.rgba.chunks_exact_mut(4) {
118        let mut r = px[0] as f32;
119        let mut g = px[1] as f32;
120        let mut b = px[2] as f32;
121        if mono_amt > 0.0 {
122            let gray = (0.299 * r + 0.587 * g + 0.114 * b).round();
123            r = r * (1.0 - mono_amt) + gray * mono_amt;
124            g = g * (1.0 - mono_amt) + gray * mono_amt;
125            b = b * (1.0 - mono_amt) + gray * mono_amt;
126        }
127        let ri = r.round().clamp(0.0, 255.0) as usize;
128        let gi = g.round().clamp(0.0, 255.0) as usize;
129        let bi = b.round().clamp(0.0, 255.0) as usize;
130        px[0] = row.lut_r[ri];
131        px[1] = row.lut_g[gi];
132        px[2] = row.lut_b[bi];
133    }
134    out
135}
136
137fn find_tonecurve_path(project_dir: &Path) -> Option<PathBuf> {
138    let cfg = load_gameexe_config(project_dir)?;
139    let rel = cfg.get_unquoted("TONECURVE_FILE")?;
140    if rel.trim().is_empty() {
141        return None;
142    }
143    let p = PathBuf::from(rel.trim());
144    if p.is_absolute() {
145        return p.is_file().then_some(p);
146    }
147    let direct = project_dir.join(&p);
148    if direct.is_file() {
149        return Some(direct);
150    }
151    let dat = project_dir.join("dat").join(&p);
152    if dat.is_file() {
153        return Some(dat);
154    }
155    None
156}
157
158fn load_gameexe_config(project_dir: &Path) -> Option<GameexeConfig> {
159    let path = find_gameexe_path(project_dir)?;
160    let raw = std::fs::read(&path).ok()?;
161    if path
162        .extension()
163        .and_then(|s| s.to_str())
164        .is_some_and(|ext| ext.eq_ignore_ascii_case("ini"))
165    {
166        let text = String::from_utf8(raw).ok()?;
167        return Some(GameexeConfig::from_text(&text));
168    }
169    let opt = GameexeDecodeOptions::from_project_dir(project_dir).ok()?;
170    let (text, _report) = decode_gameexe_dat_bytes(&raw, &opt).ok()?;
171    Some(GameexeConfig::from_text(&text))
172}
173
174fn find_gameexe_path(project_dir: &Path) -> Option<PathBuf> {
175    const CANDIDATES: &[&str] = &[
176        "Gameexe.dat",
177        "Gameexe.ini",
178        "gameexe.dat",
179        "gameexe.ini",
180        "GameexeEN.dat",
181        "GameexeEN.ini",
182        "GameexeZH.dat",
183        "GameexeZH.ini",
184        "GameexeZHTW.dat",
185        "GameexeZHTW.ini",
186        "GameexeDE.dat",
187        "GameexeDE.ini",
188        "GameexeES.dat",
189        "GameexeES.ini",
190        "GameexeFR.dat",
191        "GameexeFR.ini",
192        "GameexeID.dat",
193        "GameexeID.ini",
194    ];
195    for name in CANDIDATES {
196        let p = project_dir.join(name);
197        if p.is_file() {
198            return Some(p);
199        }
200    }
201    None
202}
203
204fn read_i32_le(data: &[u8], off: usize) -> Option<i32> {
205    let bytes: [u8; 4] = data.get(off..off + 4)?.try_into().ok()?;
206    Some(i32::from_le_bytes(bytes))
207}
208
209fn load_tonecurve_rows(path: &Path) -> Result<Vec<Option<ToneCurveRow>>> {
210    let data = std::fs::read(path)?;
211    if data.len() < 8 {
212        anyhow::bail!("tonecurve file too small");
213    }
214    let cnt = read_i32_le(&data, 4).unwrap_or(0).clamp(0, 256) as usize;
215    let mut out = vec![None; 256];
216    for i in 0..cnt {
217        let off = read_i32_le(&data, 8 + i * 4).unwrap_or(0);
218        if off <= 0 {
219            continue;
220        }
221        let off = off as usize;
222        if off + 16 * 4 + 768 > data.len() {
223            continue;
224        }
225        let typ = read_i32_le(&data, off).unwrap_or(0);
226        let base = off + 16 * 4;
227        let mut lut_r = [0u8; 256];
228        let mut lut_g = [0u8; 256];
229        let mut lut_b = [0u8; 256];
230        lut_r.copy_from_slice(&data[base..base + 256]);
231        lut_g.copy_from_slice(&data[base + 256..base + 512]);
232        lut_b.copy_from_slice(&data[base + 512..base + 768]);
233        let sat = if typ == 1 {
234            read_i32_le(&data, base + 768).unwrap_or(0)
235        } else {
236            0
237        };
238        out[i] = Some(ToneCurveRow {
239            lut_r,
240            lut_g,
241            lut_b,
242            sat,
243        });
244    }
245    Ok(out)
246}