diff --git a/CLAUDE.md b/CLAUDE.md index 8e1a30f..47c905f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,4 +38,14 @@ Rays.rust is a raytracer written in Rust with the following features: - Modules organized by functionality (material, shapes, procedural) - Scene definitions stored in JSON files (in demo/ and demo/scenes/) - Rendering progress tracking via indicatif -- Sample scenes available in demo/scenes/ with corresponding .png outputs \ No newline at end of file +- Sample scenes available in demo/scenes/ with corresponding .png outputs + +## Development Best Practices +- Make small, incremental changes focused on a single concern +- Break complex features into smaller, sequential commits +- Commit frequently to create checkpoints (after each logical change) +- Run tests and linting before each commit +- Create dedicated branches for each distinct feature +- Write descriptive commit messages explaining the purpose of changes +- Keep feature branches simple and focused on a single goal +- Verify each change works before moving to the next step \ No newline at end of file diff --git a/demo/scenes/noise-medium-test.json b/demo/scenes/noise-medium-test.json new file mode 100644 index 0000000..9b73b68 --- /dev/null +++ b/demo/scenes/noise-medium-test.json @@ -0,0 +1,150 @@ +{ + "width": 640, + "height": 480, + + "supersamples": 35, + "background": [0.2, 0.2, 0.2], + + "chunk_size": 64, + "samples_per_chunk": 2, + "shadow_bias": 0.0001, + "max_depth": 2, + + "materials": { + "RED_PLASTIC": { + "type": "lambertian", + "albedo": [0.9, 0.1, 0.1] + }, + "BLUE_PLASTIC": { + "type": "lambertian", + "albedo": [0.1, 0.1, 0.9] + }, + "GREEN_PLASTIC": { + "type": "lambertian", + "albedo": [0.1, 0.9, 0.1] + }, + "GOLD": { + "type": "metal", + "reflective": [1, 0.85, 0.57], + "roughness": 0.1 + }, + "GLASS": { + "type": "dielectric", + "refractive_index": 1.5, + "attenuate": [0.95, 0.95, 0.95] + }, + "WHITE_MARBLE": { + "type": "lambertian", + "albedo": [0.9, 0.9, 0.9] + }, + "BLACK_MARBLE": { + "type": "lambertian", + "albedo": [0.1, 0.1, 0.1] + } + }, + "media": { + "PERLIN_NOISE_MEDIUM": { + "type": "noise_medium", + "m1": "RED_PLASTIC", + "m2": "BLUE_PLASTIC", + "noise_type": "perlin", + "scale": 0.2, + "threshold": 0.5 + }, + "FBM_NOISE_MEDIUM": { + "type": "noise_medium", + "m1": "GREEN_PLASTIC", + "m2": "GOLD", + "noise_type": "fbm", + "scale": 0.2, + "threshold": 0.5, + "octaves": 4, + "persistence": 0.5, + "lacunarity": 2.0 + }, + "MARBLE_NOISE_MEDIUM": { + "type": "noise_medium", + "m1": "WHITE_MARBLE", + "m2": "BLACK_MARBLE", + "noise_type": "marble", + "scale": 0.05, + "threshold": 0.5 + }, + "WORLEY_NOISE_MEDIUM": { + "type": "noise_medium", + "m1": "GLASS", + "m2": "GOLD", + "noise_type": "worley", + "scale": 0.5, + "threshold": 0.5, + "point_density": 2.0, + "seed": 42 + }, + "COMBINED_NOISE_MEDIUM": { + "type": "noise_medium", + "m1": "RED_PLASTIC", + "m2": "GREEN_PLASTIC", + "noise_type": "combined", + "scale": 0.1, + "threshold": 0.5, + "falloff": 0.05 + }, + "CHECKERED_MARBLE": { + "type": "checkered-y-plane", + "m1": "WHITE_MARBLE", + "m2": "BLACK_MARBLE" + } + }, + + "camera": { + "location": [5, 5, -15], + "lookat" : [0, 2, 0], + "up" : [0, 1, 0], + "angle": 0.8, + "aperture": 0.1 + }, + + "lights" : [], + "variables" : {}, + + "objects": [ + { + "type": "sphere", + "radius": 2, + "location": [-5, 2, 0], + "medium" : "PERLIN_NOISE_MEDIUM" + }, + { + "type": "sphere", + "radius": 2, + "location": [-2, 2, 4], + "medium" : "FBM_NOISE_MEDIUM" + }, + { + "type": "sphere", + "radius": 2, + "location": [2, 2, 4], + "medium" : "MARBLE_NOISE_MEDIUM" + }, + { + "type": "sphere", + "radius": 2, + "location": [5, 2, 0], + "medium" : "WORLEY_NOISE_MEDIUM" + }, + { + "type": "sphere", + "radius": 2, + "location": [0, 2, -5], + "medium" : "COMBINED_NOISE_MEDIUM" + }, + { + "type" : "checkeredplane", + "y": 0, + "medium" : "CHECKERED_MARBLE" + }, + { + "type" : "skysphere" + } + ] +} \ No newline at end of file diff --git a/demo/scenes/noise-medium-test.png b/demo/scenes/noise-medium-test.png new file mode 100644 index 0000000..74dbef5 Binary files /dev/null and b/demo/scenes/noise-medium-test.png differ diff --git a/demo/scenes/noise-simple-demo.json b/demo/scenes/noise-simple-demo.json new file mode 100644 index 0000000..d86673a --- /dev/null +++ b/demo/scenes/noise-simple-demo.json @@ -0,0 +1,169 @@ +{ + "width": 800, + "height": 600, + + "supersamples": 35, + "background": [0.05, 0.05, 0.1], + + "chunk_size": 64, + "samples_per_chunk": 2, + "shadow_bias": 0.0001, + "max_depth": 2, + + "materials": { + "BRIGHT_RED": { + "type": "lambertian", + "albedo": [1.0, 0.0, 0.0] + }, + "BRIGHT_GREEN": { + "type": "lambertian", + "albedo": [0.0, 1.0, 0.0] + }, + "BRIGHT_BLUE": { + "type": "lambertian", + "albedo": [0.0, 0.0, 1.0] + }, + "BRIGHT_YELLOW": { + "type": "lambertian", + "albedo": [1.0, 1.0, 0.0] + }, + "BRIGHT_CYAN": { + "type": "lambertian", + "albedo": [0.0, 1.0, 1.0] + }, + "BRIGHT_MAGENTA": { + "type": "lambertian", + "albedo": [1.0, 0.0, 1.0] + }, + "WHITE": { + "type": "lambertian", + "albedo": [1.0, 1.0, 1.0] + }, + "BLACK": { + "type": "lambertian", + "albedo": [0.0, 0.0, 0.0] + } + }, + "media": { + "PERLIN_NOISE": { + "type": "noise_medium", + "m1": "BRIGHT_RED", + "m2": "BRIGHT_BLUE", + "noise_type": "perlin", + "scale": 0.15, + "threshold": 0.5 + }, + "FBM_NOISE": { + "type": "noise_medium", + "m1": "BRIGHT_GREEN", + "m2": "BRIGHT_MAGENTA", + "noise_type": "fbm", + "scale": 0.15, + "threshold": 0.5, + "octaves": 4, + "persistence": 0.5, + "lacunarity": 2.0 + }, + "MARBLE_NOISE": { + "type": "noise_medium", + "m1": "WHITE", + "m2": "BLACK", + "noise_type": "marble", + "scale": 0.05, + "threshold": 0.5 + }, + "WORLEY_NOISE": { + "type": "noise_medium", + "m1": "BRIGHT_CYAN", + "m2": "BRIGHT_YELLOW", + "noise_type": "worley", + "scale": 0.4, + "threshold": 0.5, + "point_density": 2.0, + "seed": 42 + }, + "COMBINED_NOISE": { + "type": "noise_medium", + "m1": "BRIGHT_RED", + "m2": "BRIGHT_GREEN", + "noise_type": "combined", + "scale": 0.1, + "threshold": 0.5, + "falloff": 0.05 + }, + "TURBULENCE_NOISE": { + "type": "noise_medium", + "m1": "BRIGHT_BLUE", + "m2": "BRIGHT_YELLOW", + "noise_type": "turbulence", + "scale": 0.15, + "threshold": 0.5, + "octaves": 4, + "persistence": 0.5, + "lacunarity": 2.0 + }, + "CHECKERED_FLOOR": { + "type": "checkered-y-plane", + "m1": "WHITE", + "m2": "BLACK" + } + }, + + "camera": { + "location": [0, 6, -18], + "lookat" : [0, 3, 0], + "up" : [0, 1, 0], + "angle": 0.8, + "aperture": 0.05 + }, + + "lights" : [], + "variables" : {}, + + "objects": [ + { + "type": "sphere", + "radius": 2, + "location": [-6, 3, 0], + "medium" : "PERLIN_NOISE" + }, + { + "type": "sphere", + "radius": 2, + "location": [-3, 3, 3], + "medium" : "FBM_NOISE" + }, + { + "type": "sphere", + "radius": 2, + "location": [0, 3, 4], + "medium" : "MARBLE_NOISE" + }, + { + "type": "sphere", + "radius": 2, + "location": [3, 3, 3], + "medium" : "WORLEY_NOISE" + }, + { + "type": "sphere", + "radius": 2, + "location": [6, 3, 0], + "medium" : "COMBINED_NOISE" + }, + { + "type": "sphere", + "radius": 2, + "location": [0, 3, -3], + "medium" : "TURBULENCE_NOISE" + }, + { + "type" : "checkeredplane", + "y": 0, + "medium" : "CHECKERED_FLOOR" + }, + { + "type" : "skysphere" + } + ] +} \ No newline at end of file diff --git a/demo/scenes/noise-simple-demo.png b/demo/scenes/noise-simple-demo.png new file mode 100644 index 0000000..336dd73 Binary files /dev/null and b/demo/scenes/noise-simple-demo.png differ diff --git a/demo/scenes/noise-test-updated.json b/demo/scenes/noise-test-updated.json new file mode 100644 index 0000000..8159489 --- /dev/null +++ b/demo/scenes/noise-test-updated.json @@ -0,0 +1,157 @@ +{ + "width": 640, + "height": 480, + + "supersamples": 100, + "background": [0.2, 0.2, 0.2], + + "chunk_size": 64, + "samples_per_chunk": 2, + "shadow_bias": 0.0001, + "max_depth": 2, + + "materials": { + "WHITE_PLASTIC": { + "type": "lambertian", + "albedo": [0.9, 0.9, 0.9] + }, + "RED_MATERIAL": { + "type": "lambertian", + "albedo": [0.8, 0.1, 0.1] + }, + "GREEN_MATERIAL": { + "type": "lambertian", + "albedo": [0.1, 0.8, 0.1] + }, + "BLUE_MATERIAL": { + "type": "lambertian", + "albedo": [0.1, 0.1, 0.8] + }, + "YELLOW_MATERIAL": { + "type": "lambertian", + "albedo": [0.8, 0.8, 0.2] + }, + "MAGENTA_MATERIAL": { + "type": "lambertian", + "albedo": [0.8, 0.2, 0.8] + }, + "GOLD": { + "type": "metal", + "reflective": [1, 0.85, 0.57], + "roughness": 0.1 + }, + "WHITE_MARBLE": { + "type": "lambertian", + "albedo": [0.9, 0.9, 0.9] + }, + "BLACK_MARBLE": { + "type": "lambertian", + "albedo": [0.1, 0.1, 0.1] + } + }, + "media": { + "PERLIN_NOISE_MEDIUM": { + "type": "noise_medium", + "m1": "WHITE_PLASTIC", + "m2": "RED_MATERIAL", + "noise_type": "perlin", + "scale": 0.2, + "threshold": 0.5 + }, + "FBM_NOISE_MEDIUM": { + "type": "noise_medium", + "m1": "WHITE_PLASTIC", + "m2": "GREEN_MATERIAL", + "noise_type": "fbm", + "scale": 0.2, + "threshold": 0.5, + "octaves": 4, + "persistence": 0.5, + "lacunarity": 2.0 + }, + "MARBLE_NOISE_MEDIUM": { + "type": "noise_medium", + "m1": "WHITE_PLASTIC", + "m2": "BLUE_MATERIAL", + "noise_type": "marble", + "scale": 0.05, + "threshold": 0.5 + }, + "TURBULENCE_NOISE_MEDIUM": { + "type": "noise_medium", + "m1": "GOLD", + "m2": "YELLOW_MATERIAL", + "noise_type": "turbulence", + "scale": 0.1, + "threshold": 0.4, + "octaves": 4 + }, + "WORLEY_NOISE_MEDIUM": { + "type": "noise_medium", + "m1": "WHITE_PLASTIC", + "m2": "MAGENTA_MATERIAL", + "noise_type": "worley", + "scale": 0.5, + "threshold": 0.5, + "point_density": 2.0, + "seed": 42 + }, + "CHECKERED_MARBLE": { + "type": "checkered-y-plane", + "m1": "WHITE_MARBLE", + "m2": "BLACK_MARBLE" + } + }, + + "camera": { + "location": [5, 5, -15], + "lookat" : [0, 2, 0], + "up" : [0, 1, 0], + "angle": 0.8, + "aperture": 0.1 + }, + + "lights" : [], + "variables" : {}, + + "objects": [ + { + "type": "sphere", + "radius": 2, + "location": [-5, 2, 0], + "medium" : "PERLIN_NOISE_MEDIUM" + }, + { + "type": "sphere", + "radius": 2, + "location": [-2, 2, 4], + "medium" : "FBM_NOISE_MEDIUM" + }, + { + "type": "sphere", + "radius": 2, + "location": [2, 2, 4], + "medium" : "MARBLE_NOISE_MEDIUM" + }, + { + "type": "sphere", + "radius": 2, + "location": [5, 2, 0], + "medium" : "TURBULENCE_NOISE_MEDIUM" + }, + { + "type": "sphere", + "radius": 2, + "location": [0, 2, -5], + "medium" : "WORLEY_NOISE_MEDIUM" + }, + { + "type" : "checkeredplane", + "y": 0, + "medium" : "CHECKERED_MARBLE" + }, + { + "type" : "skysphere" + } + ] +} \ No newline at end of file diff --git a/demo/scenes/noise-test-updated.png b/demo/scenes/noise-test-updated.png new file mode 100644 index 0000000..cb78bc3 Binary files /dev/null and b/demo/scenes/noise-test-updated.png differ diff --git a/demo/scenes/noise-test.png b/demo/scenes/noise-test.png new file mode 100644 index 0000000..c8e6b7c Binary files /dev/null and b/demo/scenes/noise-test.png differ diff --git a/src/color.rs b/src/color.rs index 147444c..ea71925 100644 --- a/src/color.rs +++ b/src/color.rs @@ -55,6 +55,25 @@ impl Color { if self.rgb.z.is_nan() { 0. } else { self.rgb.z }, ); } + + /// Blend this color with another color using the given factor + /// + /// # Arguments + /// * `other` - The color to blend with + /// * `factor` - The blend factor (0.0 = this color only, 1.0 = other color only) + /// + /// # Returns + /// A new color that is a blend of this color and the other color + pub fn blend(&self, other: &Color, factor: f64) -> Color { + let clamped_factor = factor.max(0.0).min(1.0); + let self_factor = 1.0 - clamped_factor; + + Color::new( + self.rgb.x * self_factor + other.rgb.x * clamped_factor, + self.rgb.y * self_factor + other.rgb.y * clamped_factor, + self.rgb.z * self_factor + other.rgb.z * clamped_factor, + ) + } } diff --git a/src/main.rs b/src/main.rs index 305ee84..412d555 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,7 @@ mod material { pub mod functions; pub mod legacy; pub mod plastic; + pub mod noise; } mod intersection; mod sceneobject; @@ -67,6 +68,7 @@ mod procedural { pub mod fireworks; } mod participatingmedia; +mod noise; use crate::trace::trace; use crate::rendercontext::RenderContext; diff --git a/src/material/mod.rs b/src/material/mod.rs index 26e6849..d82c6ea 100644 --- a/src/material/mod.rs +++ b/src/material/mod.rs @@ -60,6 +60,23 @@ pub enum SamplesRequired { Many, // Can only be derived from a Monte-Carlo integration of many samples. } +// Module declarations +pub mod ambient; +pub mod dielectric; +pub mod diffuse_light; +pub mod functions; +pub mod lambertian; +pub mod legacy; +pub mod model; +pub mod normal; +pub mod noise; +pub mod plastic; +pub mod specular; +pub mod texture; + +// Re-export noise module components for external use +pub use noise::{NoiseTexture, NoiseType}; + /* pub trait BSDFToRename{ diff --git a/src/material/noise.rs b/src/material/noise.rs new file mode 100644 index 0000000..6947aef --- /dev/null +++ b/src/material/noise.rs @@ -0,0 +1,302 @@ +use crate::color::Color; +use crate::ray::Ray; +use crate::intersection::Intersection; +use crate::scene::Scene; +use crate::material::model::{MaterialModel, ScatteredRay}; +use crate::na::Vector3; +use crate::noise::{PerlinNoise, WorleyNoise}; + +/// Noise texture material that modifies the color of a base material +/// based on noise functions. +pub struct NoiseTexture { + /// Base material model to apply noise to + pub base_material: Box, + /// Scale factor for noise coordinates + pub scale: f64, + /// Color to blend with base material + pub color: Color, + /// Blend factor between base material and noise color (0.0 = base material only, 1.0 = noise only) + pub blend_factor: f64, + /// Perlin noise generator + perlin: PerlinNoise, + /// Worley noise generator + worley: Option, + /// Type of noise to use + pub noise_type: NoiseType, +} + +/// Different types of noise patterns that can be applied +pub enum NoiseType { + /// Basic Perlin noise + Perlin, + /// Fractal Brownian Motion based on Perlin noise + Fbm { + octaves: u32, + persistence: f64, + lacunarity: f64, + }, + /// Worley (cellular) noise + Worley { + point_density: f64, + seed: u32, + }, + /// Combined Perlin and Worley noise + Marble, + /// Turbulence pattern based on Perlin noise + Turbulence { + octaves: u32, + }, +} + +impl NoiseTexture { + /// Create a new noise texture with Perlin noise + pub fn new_perlin( + base_material: Box, + color: Color, + scale: f64, + blend_factor: f64, + ) -> Self { + Self { + base_material, + scale, + color, + blend_factor, + perlin: PerlinNoise::new(), + worley: None, + noise_type: NoiseType::Perlin, + } + } + + /// Create a new noise texture with FBM noise + pub fn new_fbm( + base_material: Box, + color: Color, + scale: f64, + blend_factor: f64, + octaves: u32, + persistence: f64, + lacunarity: f64, + ) -> Self { + Self { + base_material, + scale, + color, + blend_factor, + perlin: PerlinNoise::new(), + worley: None, + noise_type: NoiseType::Fbm { + octaves, + persistence, + lacunarity, + }, + } + } + + /// Create a new noise texture with Worley noise + pub fn new_worley( + base_material: Box, + color: Color, + scale: f64, + blend_factor: f64, + point_density: f64, + seed: u32, + ) -> Self { + Self { + base_material, + scale, + color, + blend_factor, + perlin: PerlinNoise::new(), + worley: Some(WorleyNoise::new(point_density, seed)), + noise_type: NoiseType::Worley { + point_density, + seed, + }, + } + } + + /// Create a new noise texture with marble pattern + pub fn new_marble( + base_material: Box, + color: Color, + scale: f64, + blend_factor: f64, + ) -> Self { + Self { + base_material, + scale, + color, + blend_factor, + perlin: PerlinNoise::new(), + worley: Some(WorleyNoise::new(1.0, 42)), + noise_type: NoiseType::Marble, + } + } + + /// Create a new noise texture with turbulence pattern + pub fn new_turbulence( + base_material: Box, + color: Color, + scale: f64, + blend_factor: f64, + octaves: u32, + ) -> Self { + Self { + base_material, + scale, + color, + blend_factor, + perlin: PerlinNoise::new(), + worley: None, + noise_type: NoiseType::Turbulence { octaves }, + } + } + + /// Generate turbulence value at a point + fn turbulence(&self, p: Vector3, octaves: u32) -> f64 { + let mut value = 0.0; + let mut temp_p = p; + let mut weight = 1.0; + + for _ in 0..octaves { + value += weight * self.perlin.noise(temp_p.x, temp_p.y, temp_p.z).abs(); + weight *= 0.5; + temp_p *= 2.0; + } + + value + } + + /// Calculate noise value at a point based on the selected noise type + fn noise_value(&self, p: Vector3) -> f64 { + let scaled_p = p * self.scale; + + match &self.noise_type { + NoiseType::Perlin => { + // Map from [-1,1] to [0,1] + (self.perlin.noise(scaled_p.x, scaled_p.y, scaled_p.z) + 1.0) * 0.5 + } + NoiseType::Fbm { octaves, persistence, lacunarity } => { + self.perlin.fbm(scaled_p.x, scaled_p.y, scaled_p.z, *octaves, *persistence, *lacunarity) + } + NoiseType::Worley { .. } => { + if let Some(worley) = &self.worley { + let value = worley.noise(scaled_p.x, scaled_p.y, scaled_p.z); + // Normalize Worley noise to [0,1] range (approximately) + (1.0 - value.min(1.0)).max(0.0) + } else { + 0.5 // Fallback if Worley noise is not initialized + } + } + NoiseType::Marble => { + let pattern = scaled_p.x + + self.perlin.fbm( + scaled_p.x, + scaled_p.y, + scaled_p.z, + 4, 0.5, 2.0 + ) * 10.0; + + (pattern.sin() * 0.5 + 0.5).abs() + } + NoiseType::Turbulence { octaves } => { + self.turbulence(scaled_p, *octaves) + } + } + } +} + +impl MaterialModel for NoiseTexture { + fn scatter(&self, r: &Ray, intersection: &Intersection, s: &Scene) -> ScatteredRay { + // Get the base material's scatter result + let base_scatter = self.base_material.scatter(r, intersection, s); + + // Calculate noise value at the intersection point + let noise_value = self.noise_value(intersection.point); + + // Blend the base material's color with the noise color based on the noise value + let noise_influence = noise_value * self.blend_factor; + let blended_color = base_scatter.attenuate.blend(&self.color, noise_influence); + + // Return a new scattered ray with the blended color + ScatteredRay { + ray: base_scatter.ray, + attenuate: blended_color, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::material::lambertian::Lambertian; + + #[test] + fn test_noise_texture_perlin() { + let base_material = Box::new(Lambertian { albedo: Color::white() }); + let noise_texture = NoiseTexture::new_perlin( + base_material, + Color::new(1.0, 0.0, 0.0), // Red noise color + 0.1, // Scale + 0.5, // Blend factor + ); + + // Check that noise values are in the expected range [0,1] + for x in 0..5 { + for y in 0..5 { + for z in 0..5 { + let pos = Vector3::new(x as f64, y as f64, z as f64); + let value = noise_texture.noise_value(pos); + assert!(value >= 0.0 && value <= 1.0, "Noise value out of range: {}", value); + } + } + } + } + + #[test] + fn test_noise_texture_fbm() { + let base_material = Box::new(Lambertian { albedo: Color::white() }); + let noise_texture = NoiseTexture::new_fbm( + base_material, + Color::new(0.0, 1.0, 0.0), // Green noise color + 0.1, // Scale + 0.5, // Blend factor + 4, // Octaves + 0.5, // Persistence + 2.0, // Lacunarity + ); + + // Check that fbm values are in the expected range [0,1] + for x in 0..5 { + for y in 0..5 { + for z in 0..5 { + let pos = Vector3::new(x as f64, y as f64, z as f64); + let value = noise_texture.noise_value(pos); + assert!(value >= 0.0 && value <= 1.0, "FBM value out of range: {}", value); + } + } + } + } + + #[test] + fn test_noise_texture_marble() { + let base_material = Box::new(Lambertian { albedo: Color::white() }); + let noise_texture = NoiseTexture::new_marble( + base_material, + Color::new(0.0, 0.0, 1.0), // Blue noise color + 0.1, // Scale + 0.5, // Blend factor + ); + + // Check that marble values are in the expected range [0,1] + for x in 0..5 { + for y in 0..5 { + for z in 0..5 { + let pos = Vector3::new(x as f64, y as f64, z as f64); + let value = noise_texture.noise_value(pos); + assert!(value >= 0.0 && value <= 1.0, "Marble value out of range: {}", value); + } + } + } + } +} \ No newline at end of file diff --git a/src/material/texture.rs b/src/material/texture.rs index 14dd985..8117352 100644 --- a/src/material/texture.rs +++ b/src/material/texture.rs @@ -1,5 +1,6 @@ use crate::na::{Vector3}; use crate::material::model::MaterialModel; +use crate::noise::{PerlinNoise, WorleyNoise, combined_noise}; pub trait Medium : Sync{ fn material_at(&self, pt: Vector3) -> &Box; @@ -35,3 +36,246 @@ impl Medium for CheckeredYPlane { } } +/// A medium that mixes between two materials based on noise patterns +pub struct NoiseMedium { + /// First material (used where noise value is low) + pub m1: Box, + /// Second material (used where noise value is high) + pub m2: Box, + /// Noise type specifies the kind of noise pattern to use + pub noise_type: NoiseType, + /// Scale factor for noise coordinates + pub scale: f64, + /// Threshold value for determining material selection (0.0-1.0) + pub threshold: f64, + /// Perlin noise generator + perlin: PerlinNoise, + /// Worley noise generator (optional, used by some noise types) + worley: Option, +} + +/// Different types of noise patterns available for the NoiseMedium +pub enum NoiseType { + /// Basic Perlin noise + Perlin, + /// Fractal Brownian Motion based on Perlin noise + Fbm { + octaves: u32, + persistence: f64, + lacunarity: f64, + }, + /// Worley (cellular) noise + Worley { + point_density: f64, + seed: u32, + }, + /// Combined Perlin and Worley noise to create marble-like patterns + Marble, + /// Turbulence pattern based on Perlin noise + Turbulence { + octaves: u32, + }, + /// Combined noise with distance-based falloff + Combined { + falloff: f64, + }, +} + +impl NoiseMedium { + /// Create a new noise medium with Perlin noise + pub fn new_perlin( + m1: Box, + m2: Box, + scale: f64, + threshold: f64, + ) -> Self { + Self { + m1, + m2, + scale, + threshold, + perlin: PerlinNoise::new(), + worley: None, + noise_type: NoiseType::Perlin, + } + } + + /// Create a new noise medium with FBM noise + pub fn new_fbm( + m1: Box, + m2: Box, + scale: f64, + threshold: f64, + octaves: u32, + persistence: f64, + lacunarity: f64, + ) -> Self { + Self { + m1, + m2, + scale, + threshold, + perlin: PerlinNoise::new(), + worley: None, + noise_type: NoiseType::Fbm { + octaves, + persistence, + lacunarity, + }, + } + } + + /// Create a new noise medium with Worley noise + pub fn new_worley( + m1: Box, + m2: Box, + scale: f64, + threshold: f64, + point_density: f64, + seed: u32, + ) -> Self { + Self { + m1, + m2, + scale, + threshold, + perlin: PerlinNoise::new(), + worley: Some(WorleyNoise::new(point_density, seed)), + noise_type: NoiseType::Worley { + point_density, + seed, + }, + } + } + + /// Create a new noise medium with marble pattern + pub fn new_marble( + m1: Box, + m2: Box, + scale: f64, + threshold: f64, + ) -> Self { + Self { + m1, + m2, + scale, + threshold, + perlin: PerlinNoise::new(), + worley: Some(WorleyNoise::new(1.0, 42)), + noise_type: NoiseType::Marble, + } + } + + /// Create a new noise medium with turbulence pattern + pub fn new_turbulence( + m1: Box, + m2: Box, + scale: f64, + threshold: f64, + octaves: u32, + ) -> Self { + Self { + m1, + m2, + scale, + threshold, + perlin: PerlinNoise::new(), + worley: None, + noise_type: NoiseType::Turbulence { octaves }, + } + } + + /// Create a new noise medium with combined noise + pub fn new_combined( + m1: Box, + m2: Box, + scale: f64, + threshold: f64, + falloff: f64, + ) -> Self { + Self { + m1, + m2, + scale, + threshold, + perlin: PerlinNoise::new(), + worley: Some(WorleyNoise::new(1.0, 42)), + noise_type: NoiseType::Combined { falloff }, + } + } + + /// Generate turbulence value at a point + fn turbulence(&self, p: Vector3, octaves: u32) -> f64 { + let mut value = 0.0; + let mut temp_p = p; + let mut weight = 1.0; + + for _ in 0..octaves { + value += weight * self.perlin.noise(temp_p.x, temp_p.y, temp_p.z).abs(); + weight *= 0.5; + temp_p *= 2.0; + } + + value + } + + /// Calculate noise value at a point based on the selected noise type + fn noise_value(&self, p: Vector3) -> f64 { + let scaled_p = p * self.scale; + + match &self.noise_type { + NoiseType::Perlin => { + // Map from [-1,1] to [0,1] + (self.perlin.noise(scaled_p.x, scaled_p.y, scaled_p.z) + 1.0) * 0.5 + } + NoiseType::Fbm { octaves, persistence, lacunarity } => { + self.perlin.fbm(scaled_p.x, scaled_p.y, scaled_p.z, *octaves, *persistence, *lacunarity) + } + NoiseType::Worley { .. } => { + if let Some(worley) = &self.worley { + let value = worley.noise(scaled_p.x, scaled_p.y, scaled_p.z); + // Normalize Worley noise to [0,1] range (approximately) + (1.0 - value.min(1.0)).max(0.0) + } else { + 0.5 // Fallback if Worley noise is not initialized + } + } + NoiseType::Marble => { + let pattern = scaled_p.x + + self.perlin.fbm( + scaled_p.x, + scaled_p.y, + scaled_p.z, + 4, 0.5, 2.0 + ) * 10.0; + + (pattern.sin() * 0.5 + 0.5).abs() + } + NoiseType::Turbulence { octaves } => { + self.turbulence(scaled_p, *octaves) + } + NoiseType::Combined { falloff } => { + if let Some(worley) = &self.worley { + combined_noise::density_field(p, &self.perlin, worley, self.scale, *falloff) + } else { + 0.5 // Fallback if Worley noise is not initialized + } + } + } + } +} + +impl Medium for NoiseMedium { + fn material_at(&self, pt: Vector3) -> &Box { + // Calculate noise value at the point + let noise_value = self.noise_value(pt); + + // Choose material based on noise value and threshold + if noise_value >= self.threshold { + &self.m2 + } else { + &self.m1 + } + } +} + diff --git a/src/noise.rs b/src/noise.rs new file mode 100644 index 0000000..bc88838 --- /dev/null +++ b/src/noise.rs @@ -0,0 +1,455 @@ +/// Noise module for procedural patterns generation +/// +/// This module implements various noise functions used for procedural generation, +/// including 3D Perlin noise and fractal Brownian motion (fBm). +/// Used for creating procedural textures and patterns. + +use crate::na::Vector3; +use std::f64; +use std::f64::consts::PI; + +/// Perlin noise generator for 3D space +#[derive(Clone)] +pub struct PerlinNoise { + /// Permutation table for pseudo-random generation + perm: [usize; 512], + /// Gradient vectors for 3D noise + grad3: [Vector3; 12], +} + +impl PerlinNoise { + /// Create a new Perlin noise generator with the default permutation table + pub fn new() -> Self { + // Standard permutation table (0-255) + let base_perm: [usize; 256] = [ + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, + 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, + 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, + 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, + 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, + 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, + 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, + 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, + 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, + 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, + 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, + 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, + 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, + 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, + 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, + 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, + ]; + + // Double permutation array + let mut perm = [0; 512]; + for i in 0..256 { + perm[i] = base_perm[i]; + perm[i + 256] = base_perm[i]; + } + + // 12 gradient vectors for 3D noise + let grad3 = [ + Vector3::new(1.0, 1.0, 0.0), + Vector3::new(-1.0, 1.0, 0.0), + Vector3::new(1.0, -1.0, 0.0), + Vector3::new(-1.0, -1.0, 0.0), + Vector3::new(1.0, 0.0, 1.0), + Vector3::new(-1.0, 0.0, 1.0), + Vector3::new(1.0, 0.0, -1.0), + Vector3::new(-1.0, 0.0, -1.0), + Vector3::new(0.0, 1.0, 1.0), + Vector3::new(0.0, -1.0, 1.0), + Vector3::new(0.0, 1.0, -1.0), + Vector3::new(0.0, -1.0, -1.0), + ]; + + Self { perm, grad3 } + } + + /// Get noise value at a 3D point + pub fn noise(&self, x: f64, y: f64, z: f64) -> f64 { + // Unit cube that contains point + let x_i = x.floor() as i32 & 255; + let y_i = y.floor() as i32 & 255; + let z_i = z.floor() as i32 & 255; + + // Relative coordinates of point in cube + let x = x - x.floor(); + let y = y - y.floor(); + let z = z - z.floor(); + + // Compute fade curves for each coordinate + let u = self.fade(x); + let v = self.fade(y); + let w = self.fade(z); + + // Hash coordinates of the 8 cube corners + let a = self.perm[x_i as usize] + y_i as usize; + let aa = self.perm[a] + z_i as usize; + let ab = self.perm[a + 1] + z_i as usize; + let b = self.perm[(x_i + 1) as usize] + y_i as usize; + let ba = self.perm[b] + z_i as usize; + let bb = self.perm[b + 1] + z_i as usize; + + // Blend gradients from 8 corners of cube + let g1 = self.grad(self.perm[aa], x, y, z); + let g2 = self.grad(self.perm[ba], x - 1.0, y, z); + let g3 = self.grad(self.perm[ab], x, y - 1.0, z); + let g4 = self.grad(self.perm[bb], x - 1.0, y - 1.0, z); + let g5 = self.grad(self.perm[aa + 1], x, y, z - 1.0); + let g6 = self.grad(self.perm[ba + 1], x - 1.0, y, z - 1.0); + let g7 = self.grad(self.perm[ab + 1], x, y - 1.0, z - 1.0); + let g8 = self.grad(self.perm[bb + 1], x - 1.0, y - 1.0, z - 1.0); + + // Interpolate gradients + let lerp1 = self.lerp(g1, g2, u); + let lerp2 = self.lerp(g3, g4, u); + let lerp3 = self.lerp(g5, g6, u); + let lerp4 = self.lerp(g7, g8, u); + + let lerp5 = self.lerp(lerp1, lerp2, v); + let lerp6 = self.lerp(lerp3, lerp4, v); + + let result = self.lerp(lerp5, lerp6, w); + + // Scale to [-1, 1] + result + } + + /// Generate fractal Brownian motion (fBm) noise + /// + /// fBm sums multiple octaves of Perlin noise at different frequencies and amplitudes + /// to create more complex, natural-looking patterns. + /// + /// # Arguments + /// * `x`, `y`, `z` - Coordinates to sample noise at + /// * `octaves` - Number of noise layers to sum + /// * `persistence` - How much each octave's amplitude decreases (typically 0.5) + /// * `lacunarity` - How much each octave's frequency increases (typically 2.0) + pub fn fbm(&self, x: f64, y: f64, z: f64, octaves: u32, persistence: f64, lacunarity: f64) -> f64 { + let mut result = 0.0; + let mut amplitude = 1.0; + let mut frequency = 1.0; + let mut max_value = 0.0; + + for _ in 0..octaves { + result += self.noise(x * frequency, y * frequency, z * frequency) * amplitude; + max_value += amplitude; + amplitude *= persistence; + frequency *= lacunarity; + } + + // Normalize to [0, 1] + (result / max_value + 1.0) * 0.5 + } + + /// Fade function - 6t^5 - 15t^4 + 10t^3 + fn fade(&self, t: f64) -> f64 { + t * t * t * (t * (t * 6.0 - 15.0) + 10.0) + } + + /// Linear interpolation + fn lerp(&self, a: f64, b: f64, t: f64) -> f64 { + a + t * (b - a) + } + + /// Gradient function for 3D noise + fn grad(&self, hash: usize, x: f64, y: f64, z: f64) -> f64 { + // Use hash to pick one of the 12 gradient vectors + let h = hash & 11; + let grad = &self.grad3[h]; + + // Dot product of gradient vector with offset vector + grad.x * x + grad.y * y + grad.z * z + } +} + +/// Worley noise (cellular noise) generator +#[derive(Clone)] +pub struct WorleyNoise { + /// Feature points density + point_density: f64, + /// Random seed + seed: u32, +} + +impl WorleyNoise { + /// Create a new Worley noise generator + pub fn new(point_density: f64, seed: u32) -> Self { + Self { point_density, seed } + } + + /// Get noise value at a 3D point + /// + /// Returns the distance to the closest feature point. + pub fn noise(&self, x: f64, y: f64, z: f64) -> f64 { + // This is a simplified placeholder implementation + // A full implementation would use spatial hashing for efficiency + + // Simple hash function based on position and seed + let hash = |px: f64, py: f64, pz: f64, s: u32| -> f64 { + // Use bitwise XOR on integer portion converted to u32 + let ix = px.floor() as u32; + let iy = py.floor() as u32; + let iz = pz.floor() as u32; + let h = ((ix.wrapping_mul(73856093)) ^ + (iy.wrapping_mul(19349663)) ^ + (iz.wrapping_mul(83492791))).wrapping_mul(s); + // Convert back to float in range [0,1] + (h as f64 / u32::MAX as f64).sin() * 0.5 + 0.5 + }; + + // Find cell containing the point + let xi = x.floor(); + let yi = y.floor(); + let zi = z.floor(); + + let mut min_dist = f64::MAX; + + // Check neighboring cells + for dx in -1..=1 { + for dy in -1..=1 { + for dz in -1..=1 { + let cx = xi + dx as f64; + let cy = yi + dy as f64; + let cz = zi + dz as f64; + + // Random position within cell + let px = cx + hash(cx, cy, cz, self.seed); + let py = cy + hash(cx, cy, cz, self.seed + 1); + let pz = cz + hash(cx, cy, cz, self.seed + 2); + + // Calculate distance to feature point + let dx = px - x; + let dy = py - y; + let dz = pz - z; + let dist = (dx * dx + dy * dy + dz * dz).sqrt(); + + min_dist = min_dist.min(dist); + } + } + } + + // Scale by point density + min_dist * self.point_density + } +} + +/// Utility functions for combining noise types +pub mod combined_noise { + use super::*; + + /// Generate density field by combining noise types + /// + /// This function combines Perlin and Worley noise to create + /// complex density patterns with fine detail. + /// + /// # Arguments + /// * `position` - 3D position to sample + /// * `perlin` - Perlin noise generator + /// * `worley` - Worley noise generator + /// * `scale` - Overall noise scale factor + /// * `falloff` - Controls how density decreases with distance from origin + pub fn density_field( + position: Vector3, + perlin: &PerlinNoise, + worley: &WorleyNoise, + scale: f64, + falloff: f64 + ) -> f64 { + let x = position.x * scale; + let y = position.y * scale; + let z = position.z * scale; + + // Base shape from Perlin noise + let shape = perlin.fbm(x * 0.1, y * 0.1, z * 0.1, 4, 0.5, 2.0); + + // Detail from Worley noise + let detail = worley.noise(x, y, z); + + // Combine shape and detail + let raw_density = shape - detail * 0.5; + + // Apply falloff factor + let distance = position.norm(); + let falloff_factor = (-distance * falloff).exp(); + + // Ensure density is in [0, 1] range + (raw_density * falloff_factor).max(0.0).min(1.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_perlin_noise_range() { + let perlin = PerlinNoise::new(); + + // Test noise bounds + for x in 0..10 { + for y in 0..10 { + for z in 0..10 { + let n = perlin.noise(x as f64 * 0.1, y as f64 * 0.1, z as f64 * 0.1); + assert!(n >= -1.0 && n <= 1.0); + } + } + } + } + + #[test] + fn test_perlin_fbm_range() { + let perlin = PerlinNoise::new(); + + // Test fbm bounds + for x in 0..10 { + for y in 0..10 { + for z in 0..10 { + let n = perlin.fbm( + x as f64 * 0.1, + y as f64 * 0.1, + z as f64 * 0.1, + 4, 0.5, 2.0 + ); + assert!(n >= 0.0 && n <= 1.0); + } + } + } + } + + #[test] + fn test_worley_noise() { + let worley = WorleyNoise::new(1.0, 42); + + // Test worley noise is positive + for x in 0..10 { + for y in 0..10 { + for z in 0..10 { + let n = worley.noise(x as f64 * 0.1, y as f64 * 0.1, z as f64 * 0.1); + assert!(n >= 0.0); + } + } + } + } + + #[test] + fn test_density_field() { + let perlin = PerlinNoise::new(); + let worley = WorleyNoise::new(1.0, 42); + + // Test density field is in [0, 1] range + for x in 0..5 { + for y in 0..5 { + for z in 0..5 { + let pos = Vector3::new(x as f64, y as f64, z as f64); + let density = combined_noise::density_field(pos, &perlin, &worley, 0.1, 0.1); + assert!(density >= 0.0 && density <= 1.0); + } + } + } + } + + #[test] + fn test_distance_gradient() { + let perlin = PerlinNoise::new(); + let worley = WorleyNoise::new(1.0, 42); + + // Test that density decreases with distance from origin due to falloff + let scale = 0.1; + let falloff = 0.2; + + // Sample at different distances from origin + let pos_close = Vector3::new(0.0, 0.0, 0.0); + let pos_mid = Vector3::new(5.0, 0.0, 0.0); + let pos_far = Vector3::new(10.0, 0.0, 0.0); + + let density_close = combined_noise::density_field(pos_close, &perlin, &worley, scale, falloff); + let density_mid = combined_noise::density_field(pos_mid, &perlin, &worley, scale, falloff); + let density_far = combined_noise::density_field(pos_far, &perlin, &worley, scale, falloff); + + // Density should decrease with distance + // Note: This test may occasionally fail due to the nature of noise, + // but the general trend should hold across most seed values + assert!(density_close >= density_mid || density_mid >= density_far); + } + + #[test] + fn test_density_variation() { + // This test verifies the density_field function produces varied results + + // Hard-coded inputs for deterministic results + let perlin = PerlinNoise::new(); + let worley = WorleyNoise::new(1.0, 42); + let scale = 0.1; + let falloff = 0.05; + + // Sample a few specific points + let positions = vec![ + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(5.0, 5.0, 5.0), + Vector3::new(10.0, 0.0, 0.0), + Vector3::new(0.0, 10.0, 0.0), + Vector3::new(0.0, 0.0, 10.0), + ]; + + // Get densities at each position + let densities: Vec = positions.iter() + .map(|pos| combined_noise::density_field(*pos, &perlin, &worley, scale, falloff)) + .collect(); + + // Print the densities for inspection + println!("Density field values at test points: {:?}", densities); + + // Simple sanity check - make sure all results are in the valid range + for &density in &densities { + assert!(density >= 0.0 && density <= 1.0, + "Density should be in range [0,1], got {}", density); + } + } + + #[test] + fn test_noise_visualization() { + let perlin = PerlinNoise::new(); + let worley = WorleyNoise::new(1.5, 42); + let scale = 0.03; + let falloff = 0.01; // Minimal falloff for visualization + + // Generate a small grid of density values + let size = 10; + + // Print header + println!("\nNoise pattern visualization (10x10 grid):"); + println!("----------------------------------------"); + + // Use the same objects as in other tests for consistency + let perlin = perlin.clone(); + let worley = worley.clone(); + + // Print grid with ASCII density representation + for y in 0..size { + let mut line = String::new(); + for x in 0..size { + // Convert to world coordinates + let wx = (x as f64 / size as f64) * 2.0 - 1.0; + let wz = (y as f64 / size as f64) * 2.0 - 1.0; + + let pos = Vector3::new(wx * 100.0, 0.0, wz * 100.0); + let density = combined_noise::density_field( + pos, &perlin, &worley, scale, falloff + ); + + // Map density to ASCII characters + let char_idx = (density * 9.0).round() as usize; + let density_chars = " .:-=+*#%@"; + line.push(density_chars.chars().nth(char_idx).unwrap()); + line.push(' '); // Add space for better visibility + } + println!("{}", line); + } + println!("----------------------------------------"); + + // This test always passes - it's for visual inspection + assert!(true); + } +} \ No newline at end of file diff --git a/src/scenefile.rs b/src/scenefile.rs index 888b1ed..06b00a2 100644 --- a/src/scenefile.rs +++ b/src/scenefile.rs @@ -23,7 +23,7 @@ use serde_json; use std::io::prelude::*; use std::fs::File; use crate::material::model::MaterialModel; -use crate::material::texture::{Solid, CheckeredYPlane, Medium}; +use crate::material::texture::{Solid, CheckeredYPlane, Medium, NoiseMedium, self}; use crate::material::specular::Specular; use crate::material::dielectric::Dielectric; use crate::material::plastic::Plastic; @@ -31,6 +31,7 @@ use crate::material::lambertian::Lambertian; use crate::material::normal::NormalShade; use crate::material::legacy::{ Whitted, FlatColor }; use crate::material::diffuse_light::DiffuseLight; +use crate::material::noise::{NoiseTexture, NoiseType}; use crate::participatingmedia::{ParticipatingMedium, HomogenousFog, Vacuum}; use crate::shapes::geometry::Geometry; @@ -140,9 +141,27 @@ impl SceneFile { return SceneFile::parse_medium_ref(mid, materials, media).unwrap() }, None => { - // Default is Solid - let m = SceneFile::parse_material_ref(&o["material"], materials).unwrap(); - return Box::new(Solid { m: m }) + // Check if material is specified + if let Some(material_key) = o.get("material") { + // Handle direct material reference case + let material_name = SceneFile::parse_string(material_key); + + // Check if this is a direct noise material definition + if let Some(material_def) = materials.get(&material_name) { + if let Some("noise") = material_def.get("type").and_then(|v| v.as_str()) { + // This is a noise material, we need to parse it as a medium + return SceneFile::parse_medium(material_def, materials).unwrap(); + } + } + + // Default case - just use the referenced material + let m = SceneFile::parse_material_ref(material_key, materials).unwrap(); + return Box::new(Solid { m: m }) + } else { + // No material or medium specified + let default_material = Box::new(Lambertian { albedo: Color::white() }); + return Box::new(Solid { m: default_material }) + } } } } @@ -293,11 +312,17 @@ impl SceneFile { } pub fn parse_material_ref(key: &Value, materials: &Map ) -> Option> { - let props = materials.get(&SceneFile::parse_string(key)).unwrap(); - return SceneFile::parse_material(props); + if let Some(props) = materials.get(&SceneFile::parse_string(key)) { + return SceneFile::parse_material(props); + } + println!("Warning: Material '{}' not found in materials map", SceneFile::parse_string(key)); + None } pub fn parse_material(o: &Value) -> Option> { + // The noise material needs access to the entire materials dictionary to resolve + // references to base materials, but we don't have access to it here. + // We'll handle that special case in a different function. let t = o["type"].as_str().unwrap(); if t == "metal" { let metal:Specular = Specular { @@ -339,6 +364,7 @@ impl SceneFile { }; return Some(Box::new(d)); } + if t == "flat" { let d: FlatColor = FlatColor { pigment: SceneFile::parse_color(&o["color"]), @@ -358,6 +384,9 @@ impl SceneFile { if t == "normal" { return Some(Box::new(NormalShade {})); } + + // Noise materials are handled separately in parse_object_medium + /* return material::MaterialProperties { pigment: SceneFile::parse_color(&o["pigment"]), @@ -375,8 +404,14 @@ impl SceneFile { } pub fn parse_medium_ref(key: &Value, materials: &Map, media: &Map ) -> Option> { - let props = media.get(&SceneFile::parse_string(key)).unwrap(); - return SceneFile::parse_medium(props, materials); + let medium_name = SceneFile::parse_string(key); + match media.get(&medium_name) { + Some(props) => SceneFile::parse_medium(props, materials), + None => { + eprintln!("ERROR: Medium '{}' not found in media map. This is a fatal error.", medium_name); + panic!("Medium '{}' not found in media map", medium_name); + } + } } pub fn parse_medium(o: &Value, materials: &Map) -> Option> { @@ -394,7 +429,202 @@ impl SceneFile { return Some(Box::new(CheckeredYPlane { m1: m1, m2: m2, xsize: xsize, zsize: zsize })); - + } + + if t == "noise" { + // When noise is used as a medium, parse it here + // Get base material by reference + let base_material_key = &o["base_material"]; + let base_material = if let Some(material_name) = base_material_key.as_str() { + // We need to use the root-level materials map from the scene file + SceneFile::parse_material_ref(base_material_key, materials) + .unwrap_or_else(|| { + println!("Warning: Using default material for noise texture because base material '{}' not found", material_name); + Box::new(Lambertian { albedo: Color::white() }) + }) + } else { + // Default to white lambertian if no base material is specified + Box::new(Lambertian { albedo: Color::white() }) + }; + + let noise_type = match o.get("noise_type").and_then(|v| v.as_str()) { + Some("perlin") => NoiseType::Perlin, + Some("fbm") => NoiseType::Fbm { + octaves: SceneFile::parse_int(&o["octaves"], 4) as u32, + persistence: SceneFile::parse_number(&o["persistence"], 0.5), + lacunarity: SceneFile::parse_number(&o["lacunarity"], 2.0), + }, + Some("worley") => NoiseType::Worley { + point_density: SceneFile::parse_number(&o["point_density"], 1.0), + seed: SceneFile::parse_int(&o["seed"], 42) as u32, + }, + Some("marble") => NoiseType::Marble, + Some("turbulence") => NoiseType::Turbulence { + octaves: SceneFile::parse_int(&o["octaves"], 4) as u32, + }, + _ => NoiseType::Perlin, // Default to Perlin + }; + + let noise_texture = match noise_type { + NoiseType::Perlin => { + NoiseTexture::new_perlin( + base_material, + SceneFile::parse_color(&o["color"]), + SceneFile::parse_number(&o["scale"], 0.1), + SceneFile::parse_number(&o["blend_factor"], 0.5), + ) + }, + NoiseType::Fbm { octaves, persistence, lacunarity } => { + NoiseTexture::new_fbm( + base_material, + SceneFile::parse_color(&o["color"]), + SceneFile::parse_number(&o["scale"], 0.1), + SceneFile::parse_number(&o["blend_factor"], 0.5), + octaves, + persistence, + lacunarity, + ) + }, + NoiseType::Worley { point_density, seed } => { + NoiseTexture::new_worley( + base_material, + SceneFile::parse_color(&o["color"]), + SceneFile::parse_number(&o["scale"], 0.1), + SceneFile::parse_number(&o["blend_factor"], 0.5), + point_density, + seed, + ) + }, + NoiseType::Marble => { + NoiseTexture::new_marble( + base_material, + SceneFile::parse_color(&o["color"]), + SceneFile::parse_number(&o["scale"], 0.1), + SceneFile::parse_number(&o["blend_factor"], 0.5), + ) + }, + NoiseType::Turbulence { octaves } => { + NoiseTexture::new_turbulence( + base_material, + SceneFile::parse_color(&o["color"]), + SceneFile::parse_number(&o["scale"], 0.1), + SceneFile::parse_number(&o["blend_factor"], 0.5), + octaves, + ) + }, + }; + + return Some(Box::new(Solid { m: Box::new(noise_texture) })); + } + + if t == "noise_medium" { + // Get the two materials to mix between + let m1_name = SceneFile::parse_string(&o["m1"]); + let m1 = match SceneFile::parse_material_ref(&o["m1"], materials) { + Some(mat) => mat, + None => { + eprintln!("ERROR: Material '{}' not found for NoiseMedium m1. This is a fatal error.", m1_name); + panic!("Material '{}' for NoiseMedium m1 not found", m1_name); + } + }; + + let m2_name = SceneFile::parse_string(&o["m2"]); + let m2 = match SceneFile::parse_material_ref(&o["m2"], materials) { + Some(mat) => mat, + None => { + eprintln!("ERROR: Material '{}' not found for NoiseMedium m2. This is a fatal error.", m2_name); + panic!("Material '{}' for NoiseMedium m2 not found", m2_name); + } + }; + + // Parse the threshold value (default to 0.5) + let threshold = SceneFile::parse_number(&o["threshold"], 0.5); + + // Parse scale (default to 0.1) + let scale = SceneFile::parse_number(&o["scale"], 0.1); + + // Parse noise_type + let noise_type = match o.get("noise_type").and_then(|v| v.as_str()) { + Some("perlin") => texture::NoiseType::Perlin, + Some("fbm") => texture::NoiseType::Fbm { + octaves: SceneFile::parse_int(&o["octaves"], 4) as u32, + persistence: SceneFile::parse_number(&o["persistence"], 0.5), + lacunarity: SceneFile::parse_number(&o["lacunarity"], 2.0), + }, + Some("worley") => texture::NoiseType::Worley { + point_density: SceneFile::parse_number(&o["point_density"], 1.0), + seed: SceneFile::parse_int(&o["seed"], 42) as u32, + }, + Some("marble") => texture::NoiseType::Marble, + Some("turbulence") => texture::NoiseType::Turbulence { + octaves: SceneFile::parse_int(&o["octaves"], 4) as u32, + }, + Some("combined") => texture::NoiseType::Combined { + falloff: SceneFile::parse_number(&o["falloff"], 0.1), + }, + _ => texture::NoiseType::Perlin, // Default to Perlin + }; + + // Create the appropriate NoiseMedium based on the noise type + let noise_medium = match noise_type { + texture::NoiseType::Perlin => { + NoiseMedium::new_perlin( + m1, + m2, + scale, + threshold, + ) + }, + texture::NoiseType::Fbm { octaves, persistence, lacunarity } => { + NoiseMedium::new_fbm( + m1, + m2, + scale, + threshold, + octaves, + persistence, + lacunarity, + ) + }, + texture::NoiseType::Worley { point_density, seed } => { + NoiseMedium::new_worley( + m1, + m2, + scale, + threshold, + point_density, + seed, + ) + }, + texture::NoiseType::Marble => { + NoiseMedium::new_marble( + m1, + m2, + scale, + threshold, + ) + }, + texture::NoiseType::Turbulence { octaves } => { + NoiseMedium::new_turbulence( + m1, + m2, + scale, + threshold, + octaves, + ) + }, + texture::NoiseType::Combined { falloff } => { + NoiseMedium::new_combined( + m1, + m2, + scale, + threshold, + falloff, + ) + }, + }; + + return Some(Box::new(noise_medium)); } return None