siglus_scene_vm/runtime/
tonecurve.rs1use 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}