tx2_iff/
texture.rs

1//! Texture synthesis module (Layer 2: The Flesh)
2//!
3//! This module implements deterministic texture synthesis using PPF-based noise
4//! generation. Instead of storing high-entropy texture pixels, we store compact
5//! region descriptors and synthesize the texture on-the-fly during decoding.
6//!
7//! ## The Cheat
8//!
9//! Standard codecs spend huge amounts of bits encoding "noise" (pores on skin,
10//! grain in asphalt, leaves on trees). We don't store the pixels - we store a
11//! synthesizer config and generate the texture deterministically.
12//!
13//! ## Data Structure
14//!
15//! A Region stores compact texture synthesis parameters instead of raw pixels:
16//! - Position and size (x, y, w, h)
17//! - Deterministic noise seed
18//! - Chaos level (number of octaves 1-8)
19//! - Scale (base frequency log₂)
20//! - Persistence (amplitude decay 0-255 → 0.0-1.0)
21//! - Noise type (fBm, turbulence, ridged, etc.)
22//! - Base color (RGB)
23//! - Amplitude (noise amplitude 0-255)
24
25use crate::error::{IffError, Result};
26use crate::fixed::Fixed;
27use crate::noise::{NoiseParams, PerlinNoise, PpfNoise};
28use serde::{Deserialize, Serialize};
29
30/// Texture region descriptor
31#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
32pub struct Region {
33    /// X coordinate (top-left corner)
34    pub x: u16,
35    /// Y coordinate (top-left corner)
36    pub y: u16,
37    /// Width in pixels
38    pub w: u16,
39    /// Height in pixels
40    pub h: u16,
41    /// Deterministic noise seed
42    pub seed: u32,
43    /// Number of octaves (1-8)
44    pub chaos_level: u8,
45    /// Base frequency scale (log₂, 0-7)
46    pub scale: u8,
47    /// Amplitude decay between octaves (0-255 → 0.0-1.0)
48    pub persistence: u8,
49    /// Type of noise function
50    pub noise_type: NoiseType,
51    /// RGB base color
52    pub base_color: [u8; 3],
53    /// Noise amplitude (0-255)
54    pub amplitude: u8,
55}
56
57impl Region {
58    /// Create a new region with default parameters
59    pub fn new(x: u16, y: u16, w: u16, h: u16) -> Self {
60        Region {
61            x,
62            y,
63            w,
64            h,
65            seed: 0,
66            chaos_level: 4,
67            scale: 1,
68            persistence: 128, // 0.5 in u8 encoding
69            noise_type: NoiseType::Fbm,
70            base_color: [128, 128, 128],
71            amplitude: 64,
72        }
73    }
74
75    /// Check if a point is inside this region
76    pub fn contains(&self, x: u16, y: u16) -> bool {
77        x >= self.x && x < self.x + self.w && y >= self.y && y < self.y + self.h
78    }
79
80    /// Get noise parameters for this region
81    pub fn noise_params(&self) -> NoiseParams {
82        NoiseParams {
83            seed: self.seed,
84            octaves: self.chaos_level.min(8),
85            scale: self.scale.min(7),
86            persistence: self.persistence as f32 / 255.0,
87            lacunarity: 2.0,
88        }
89    }
90}
91
92/// Type of noise function to use
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94#[repr(u8)]
95pub enum NoiseType {
96    /// Fractal Brownian Motion (smooth, natural)
97    Fbm = 0,
98    /// Turbulence (absolute value, energetic)
99    Turbulence = 1,
100    /// Ridged multi-fractal (sharp features)
101    Ridged = 2,
102    /// Warped (domain distortion)
103    Warped = 3,
104    /// Cellular/Worley (organic cells)
105    Cellular = 4,
106    /// Perlin (classic gradient noise)
107    Perlin = 5,
108}
109
110impl Default for NoiseType {
111    fn default() -> Self {
112        NoiseType::Fbm
113    }
114}
115
116/// Texture synthesizer
117pub struct TextureSynthesizer {
118    /// Regions to synthesize
119    regions: Vec<Region>,
120}
121
122impl TextureSynthesizer {
123    /// Create a new synthesizer
124    pub fn new() -> Self {
125        TextureSynthesizer {
126            regions: Vec::new(),
127        }
128    }
129
130    /// Add a region
131    pub fn add_region(&mut self, region: Region) {
132        self.regions.push(region);
133    }
134
135    /// Get all regions
136    pub fn regions(&self) -> &[Region] {
137        &self.regions
138    }
139
140    /// Synthesize texture for a specific pixel
141    pub fn synthesize_pixel(&self, x: u16, y: u16) -> Option<[u8; 3]> {
142        // Find region containing this pixel
143        let region = self.regions.iter().find(|r| r.contains(x, y))?;
144
145        // Local coordinates within region
146        let local_x = x - region.x;
147        let local_y = y - region.y;
148
149        // Synthesize color
150        Some(self.synthesize_region_pixel(region, local_x, local_y))
151    }
152
153    /// Synthesize a pixel within a region
154    pub fn synthesize_region_pixel(&self, region: &Region, local_x: u16, local_y: u16) -> [u8; 3] {
155        // Convert to fixed-point coordinates
156        let x = Fixed::from(local_x);
157        let y = Fixed::from(local_y);
158
159        // Generate noise value based on type
160        let noise_val = match region.noise_type {
161            NoiseType::Fbm => {
162                let noise = PpfNoise::new(region.noise_params());
163                noise.fbm(x, y)
164            }
165            NoiseType::Turbulence => {
166                let noise = PpfNoise::new(region.noise_params());
167                noise.turbulence(x, y)
168            }
169            NoiseType::Ridged => {
170                let noise = PpfNoise::new(region.noise_params());
171                noise.ridged(x, y)
172            }
173            NoiseType::Warped => {
174                let noise = PpfNoise::new(region.noise_params());
175                let warp_strength = Fixed::from_int(2);
176                noise.warped(x, y, warp_strength)
177            }
178            NoiseType::Cellular => {
179                let noise = PpfNoise::new(region.noise_params());
180                let cell_size = Fixed::from_int(10);
181                noise.cellular(x, y, cell_size)
182            }
183            NoiseType::Perlin => {
184                let perlin = PerlinNoise::new(region.seed);
185                perlin.noise(x, y)
186            }
187        };
188
189        // Convert noise to signed value centered at 0
190        let noise_centered = noise_val - Fixed::HALF;
191
192        // Scale by amplitude
193        let amplitude = Fixed::from_f32(region.amplitude as f32 / 255.0);
194        let noise_scaled = noise_centered * amplitude * Fixed::from_int(255);
195
196        // Apply to base color
197        let mut color = [0u8; 3];
198        for i in 0..3 {
199            let base = region.base_color[i] as i32;
200            let modulated = base + noise_scaled.to_int();
201            color[i] = modulated.clamp(0, 255) as u8;
202        }
203
204        color
205    }
206
207    /// Synthesize entire region to buffer
208    pub fn synthesize_region(&self, region: &Region, buffer: &mut [[u8; 3]]) -> Result<()> {
209        if buffer.len() != (region.w as usize * region.h as usize) {
210            return Err(IffError::Other(
211                "Buffer size doesn't match region dimensions".to_string(),
212            ));
213        }
214
215        for y in 0..region.h {
216            for x in 0..region.w {
217                let idx = (y as usize * region.w as usize) + x as usize;
218                buffer[idx] = self.synthesize_region_pixel(region, x, y);
219            }
220        }
221
222        Ok(())
223    }
224}
225
226impl Default for TextureSynthesizer {
227    fn default() -> Self {
228        Self::new()
229    }
230}
231
232/// Texture analyzer for finding optimal synthesis parameters
233#[cfg(feature = "encoder")]
234pub struct TextureAnalyzer {
235    /// Similarity threshold for region detection (0.0-1.0)
236    similarity_threshold: f32,
237}
238
239#[cfg(feature = "encoder")]
240impl TextureAnalyzer {
241    /// Create a new texture analyzer
242    pub fn new(similarity_threshold: f32) -> Self {
243        TextureAnalyzer {
244            similarity_threshold,
245        }
246    }
247
248    /// Detect high-entropy regions suitable for synthesis
249    pub fn detect_texture_regions(
250        &self,
251        image: &[[u8; 3]],
252        width: usize,
253        height: usize,
254        min_size: usize,
255    ) -> Vec<Region> {
256        let mut regions = Vec::new();
257
258        // Simple grid-based region detection
259        let region_size = min_size.max(32);
260
261        for y in (0..height).step_by(region_size) {
262            for x in (0..width).step_by(region_size) {
263                let w = (region_size).min(width - x);
264                let h = (region_size).min(height - y);
265
266                if w < min_size || h < min_size {
267                    continue;
268                }
269
270                // Calculate entropy of region
271                let entropy = self.calculate_entropy(image, x, y, w, h, width);
272
273                log::debug!("Region ({}, {}) entropy: {}", x, y, entropy);
274
275                // High entropy regions are good candidates for synthesis
276                // Use the similarity threshold from the analyzer
277                if entropy > self.similarity_threshold {
278                    let region = Region::new(x as u16, y as u16, w as u16, h as u16);
279                    regions.push(region);
280                }
281            }
282        }
283
284        regions
285    }
286
287    /// Calculate entropy of a region (0.0 = uniform, 1.0 = maximum entropy)
288    fn calculate_entropy(
289        &self,
290        image: &[[u8; 3]],
291        x: usize,
292        y: usize,
293        w: usize,
294        h: usize,
295        stride: usize,
296    ) -> f32 {
297        // Calculate variance as proxy for entropy
298        let mut sum = [0f32; 3];
299        let mut sum_sq = [0f32; 3];
300        let mut count = 0;
301
302        for dy in 0..h {
303            for dx in 0..w {
304                let idx = (y + dy) * stride + (x + dx);
305                if idx < image.len() {
306                    let pixel = image[idx];
307                    for c in 0..3 {
308                        let val = pixel[c] as f32;
309                        sum[c] += val;
310                        sum_sq[c] += val * val;
311                    }
312                    count += 1;
313                }
314            }
315        }
316
317        if count == 0 {
318            return 0.0;
319        }
320
321        // Calculate average variance across channels
322        let count_f = count as f32;
323        let mut total_variance = 0.0;
324
325        for c in 0..3 {
326            let mean = sum[c] / count_f;
327            let variance = (sum_sq[c] / count_f) - (mean * mean);
328            total_variance += variance;
329        }
330
331        // Normalize to [0, 1]
332        (total_variance / (3.0 * 255.0 * 255.0)).min(1.0)
333    }
334
335    /// Find optimal synthesis parameters for a region
336    pub fn optimize_region(
337        &self,
338        image: &[[u8; 3]],
339        region: &mut Region,
340        stride: usize,
341        max_iterations: usize,
342        error_threshold: f32,
343    ) -> Result<f32> {
344        let mut best_seed = 0u32;
345        let mut best_error = f32::MAX;
346
347        // Extract region pixels
348        let region_pixels = self.extract_region(image, region, stride)?;
349
350        // Calculate base color (average)
351        let base_color = self.calculate_base_color(&region_pixels);
352        region.base_color = base_color;
353
354        // Try different seeds
355        for iteration in 0..max_iterations {
356            let seed = iteration as u32 * 12345; // Pseudo-random seed generation
357            region.seed = seed;
358
359            // Synthesize with this seed
360            let synthesizer = TextureSynthesizer::new();
361            let mut synth_buffer = vec![[0u8; 3]; region_pixels.len()];
362            synthesizer.synthesize_region(region, &mut synth_buffer)?;
363
364            // Calculate error
365            let error = self.calculate_l2_error(&region_pixels, &synth_buffer);
366
367            if error < best_error {
368                best_error = error;
369                best_seed = seed;
370            }
371
372            // Early exit if error is good enough
373            if error < error_threshold {
374                break;
375            }
376        }
377
378        log::debug!("Region optimized: best_error = {}, threshold = {}", best_error, error_threshold);
379
380        region.seed = best_seed;
381        Ok(best_error)
382    }
383
384    /// Extract region pixels from image
385    fn extract_region(&self, image: &[[u8; 3]], region: &Region, stride: usize) -> Result<Vec<[u8; 3]>> {
386        let mut pixels = Vec::with_capacity((region.w as usize) * (region.h as usize));
387
388        for y in 0..region.h {
389            for x in 0..region.w {
390                let img_x = region.x as usize + x as usize;
391                let img_y = region.y as usize + y as usize;
392                let idx = img_y * stride + img_x;
393
394                if idx < image.len() {
395                    pixels.push(image[idx]);
396                } else {
397                    pixels.push([0, 0, 0]);
398                }
399            }
400        }
401
402        Ok(pixels)
403    }
404
405    /// Calculate base color (average) of region
406    fn calculate_base_color(&self, pixels: &[[u8; 3]]) -> [u8; 3] {
407        let mut sum = [0u32; 3];
408
409        for pixel in pixels {
410            for c in 0..3 {
411                sum[c] += pixel[c] as u32;
412            }
413        }
414
415        let count = pixels.len() as u32;
416        [
417            (sum[0] / count) as u8,
418            (sum[1] / count) as u8,
419            (sum[2] / count) as u8,
420        ]
421    }
422
423    /// Calculate L2 error between two buffers
424    fn calculate_l2_error(&self, original: &[[u8; 3]], synthesized: &[[u8; 3]]) -> f32 {
425        let mut sum_sq_error = 0.0;
426
427        for (orig, synth) in original.iter().zip(synthesized.iter()) {
428            for c in 0..3 {
429                let diff = orig[c] as f32 - synth[c] as f32;
430                sum_sq_error += diff * diff;
431            }
432        }
433
434        (sum_sq_error / (original.len() as f32 * 3.0)).sqrt()
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn test_region_contains() {
444        let region = Region::new(10, 20, 100, 50);
445
446        assert!(region.contains(10, 20)); // Top-left corner
447        assert!(region.contains(109, 69)); // Bottom-right corner
448        assert!(region.contains(50, 40)); // Middle
449
450        assert!(!region.contains(9, 20)); // Just outside left
451        assert!(!region.contains(110, 40)); // Just outside right
452        assert!(!region.contains(50, 19)); // Just outside top
453        assert!(!region.contains(50, 70)); // Just outside bottom
454    }
455
456    #[test]
457    fn test_noise_params() {
458        let region = Region {
459            chaos_level: 6,
460            scale: 2,
461            persistence: 128,
462            ..Region::new(0, 0, 64, 64)
463        };
464
465        let params = region.noise_params();
466        assert_eq!(params.octaves, 6);
467        assert_eq!(params.scale, 2);
468        assert!((params.persistence - 0.5).abs() < 0.01);
469    }
470
471    #[test]
472    fn test_synthesizer() {
473        let mut synth = TextureSynthesizer::new();
474
475        let region = Region {
476            seed: 42,
477            chaos_level: 4,
478            base_color: [100, 150, 200],
479            amplitude: 50,
480            ..Region::new(0, 0, 64, 64)
481        };
482
483        synth.add_region(region);
484
485        // Synthesize a pixel
486        let pixel = synth.synthesize_pixel(10, 20);
487        assert!(pixel.is_some());
488
489        let color = pixel.unwrap();
490        // Color should be modulated around base color
491        assert!(color[0] > 50 && color[0] < 150);
492        assert!(color[1] > 100 && color[1] < 200);
493        assert!(color[2] > 150 && color[2] < 250);
494    }
495
496    #[test]
497    fn test_synthesize_determinism() {
498        let synth = TextureSynthesizer::new();
499
500        let region = Region {
501            seed: 123,
502            ..Region::new(0, 0, 64, 64)
503        };
504
505        // Same pixel should give same result
506        let color1 = synth.synthesize_region_pixel(&region, 10, 20);
507        let color2 = synth.synthesize_region_pixel(&region, 10, 20);
508
509        assert_eq!(color1, color2);
510    }
511
512    #[test]
513    fn test_synthesize_region() {
514        let synth = TextureSynthesizer::new();
515
516        let region = Region::new(0, 0, 16, 16);
517        let mut buffer = vec![[0u8; 3]; 16 * 16];
518
519        let result = synth.synthesize_region(&region, &mut buffer);
520        assert!(result.is_ok());
521
522        // Buffer should be filled
523        assert!(buffer.iter().any(|&pixel| pixel != [0, 0, 0]));
524    }
525
526    #[cfg(feature = "encoder")]
527    #[test]
528    fn test_texture_analyzer() {
529        let analyzer = TextureAnalyzer::new(0.05); // Very low threshold for test
530
531        // Create a test image with high entropy
532        let width = 64;
533        let height = 64;
534        let mut image = vec![[0u8; 3]; width * height];
535
536        // Create actual high-variance noise using multiple prime multipliers
537        for y in 0..height {
538            for x in 0..width {
539                let idx = y * width + x;
540                // Use larger primes and XOR for better distribution
541                let r = ((x * 2654435761u32 as usize + y * 2246822519u32 as usize) ^ (x * y)) % 256;
542                let g = ((x * 3266489917u32 as usize + y * 668265263u32 as usize) ^ (x + y)) % 256;
543                let b = ((x * 374761393u32 as usize + y * 1935289041u32 as usize) ^ (x | y)) % 256;
544                image[idx] = [r as u8, g as u8, b as u8];
545            }
546        }
547
548        let regions = analyzer.detect_texture_regions(&image, width, height, 16);
549
550        // Should detect at least one region with high-variance content
551        assert!(!regions.is_empty(), "Expected to detect texture regions");
552    }
553
554    #[test]
555    fn test_different_noise_types() {
556        let synth = TextureSynthesizer::new();
557
558        let noise_types = [
559            NoiseType::Fbm,
560            NoiseType::Turbulence,
561            NoiseType::Ridged,
562            NoiseType::Warped,
563            NoiseType::Cellular,
564            NoiseType::Perlin,
565        ];
566
567        for noise_type in &noise_types {
568            let region = Region {
569                noise_type: *noise_type,
570                seed: 42,
571                ..Region::new(0, 0, 64, 64)
572            };
573
574            let color = synth.synthesize_region_pixel(&region, 10, 20);
575
576            // Should produce valid color
577            assert!(color[0] <= 255);
578            assert!(color[1] <= 255);
579            assert!(color[2] <= 255);
580        }
581    }
582}