wassily-algorithms 0.2.0

Specialized rendering algorithms for wassily - a generative art library
Documentation
//! # 3D Sphere Rendering
//!
//! Advanced 3D sphere rendering with realistic lighting, texture mapping, and
//! perspective projection. This module provides a complete 3D rendering pipeline
//! optimized for generative art applications.
//!
//! ## Key Features
//!
//! - **Realistic Lighting**: Multiple light sources with diffuse and specular reflection
//! - **Texture Mapping**: Apply 2D canvases as textures on 3D spheres
//! - **3D Rotation**: Rotate spheres around X, Y, and Z axes
//! - **Perspective Projection**: Realistic perspective with configurable focal length
//! - **Multiple Lights**: Support for multiple light sources
//!
//! ## Basic Usage
//!
//! ```no_run
//! use wassily_algorithms::*;
//! use wassily_core::*;
//!
//! // Create a texture
//! let mut texture = Canvas::new(256, 256);
//! texture.fill(*BLUE);
//!
//! // Set up a basic scene
//! let scene = SphereScene::basic(pt3(0.0, 0.0, 100.0), &texture);
//!
//! // Render to output
//! let mut output = Canvas::new(800, 600);
//! render_sphere(&scene, &mut output);
//! ```
//!
//! ## Advanced Lighting
//!
//! ```no_run
//! use wassily_algorithms::*;
//! use wassily_core::*;
//!
//! let texture = Canvas::new(256, 256);
//! 
//! // Create lights
//! let lights = vec![
//!     Light::new(pt3(-10.0, 10.0, 5.0), *WHITE, 0.8),
//!     Light::new(pt3(10.0, -5.0, 10.0), *BLUE, 0.3),
//! ];
//!
//! // Set up scene with custom lighting
//! let scene = SphereScene::new(
//!     pt3(0.0, 0.0, 0.0),    // camera position
//!     400.0,                  // focal length
//!     pt3(0.0, 0.0, 100.0),  // sphere center
//!     50.0,                   // sphere radius
//!     &texture,
//!     0.1, 0.2, 0.0,         // rotation angles
//!     lights,
//!     Some(32.0),            // specular exponent
//! );
//! ```
//!
//! ## Components
//!
//! - [`SphereScene`]: Complete scene configuration
//! - [`Light`]: Light source definition
//! - Main rendering function for sphere scenes

use wassily_core::{canvas::Canvas, points::*};
use wassily_color::rgb8;
use std::f32::consts::PI;
use tiny_skia::Color;

/// Configuration for a 3D sphere rendering scene.
/// 
/// This struct contains all the parameters needed to render a textured sphere
/// with realistic lighting and perspective projection.
/// 
/// # Fields
/// 
/// - `camera`: Position of the camera in 3D space
/// - `focal_len`: Focal length for perspective projection (higher = more telephoto)
/// - `center`: Position of the sphere center in 3D space
/// - `radius`: Radius of the sphere
/// - `texture`: 2D canvas to use as texture on the sphere surface
/// - `rotation_x`, `rotation_y`, `rotation_z`: Rotation angles in radians
/// - `lights`: Vector of light sources
/// - `specular`: Optional specular reflection exponent (higher = shinier)
pub struct SphereScene<'a> {
    pub camera: Point3,
    pub focal_len: f32,
    pub center: Point3,
    pub radius: f32,
    pub texture: &'a Canvas,
    pub rotation_x: f32,
    pub rotation_y: f32,
    pub rotation_z: f32,
    pub lights: Vec<Light>,
    pub specular: Option<f32>,
}

impl<'a> SphereScene<'a> {
    pub fn new(
        camera: Point3,
        focal_len: f32,
        center: Point3,
        radius: f32,
        texture: &'a Canvas,
        rotation_x: f32,
        rotation_y: f32,
        rotation_z: f32,
        lights: Vec<Light>,
        specular: Option<f32>,
    ) -> Self {
        Self {
            camera,
            focal_len,
            center,
            radius,
            texture,
            rotation_x,
            rotation_y,
            rotation_z,
            lights,
            specular,
        }
    }

    pub fn basic(center: Point3, texture: &'a Canvas) -> Self {
        Self {
            camera: Point3::new(0.0, 0.0, 0.0),
            focal_len: texture.width() as f32,
            center,
            radius: texture.width() as f32 / 2.0,
            texture,
            rotation_x: PI / 2.0,
            rotation_y: 0.0,
            rotation_z: 0.0,
            lights: vec![],
            specular: None,
        }
    }

    pub fn color(&self, point: Point3) -> Color {
        let w = 2.0 * self.texture.width() as f32;
        let h = self.texture.height() as f32;
        let rot_point = (point - self.center)
            .rotate_x(self.rotation_x)
            .rotate_y(self.rotation_y)
            .rotate_z(self.rotation_z)
            + self.center;
        let s = rot_point.to_spherical(self.center);
        let u = w / 2.0 * (s.phi / PI + 1.0);
        let v = h * (1.0 - s.theta / PI);
        let c = self.texture.pixmap.pixel(u as u32, v as u32).unwrap();
        let mut illumination = if self.lights.is_empty() {
            1.0
        } else {
            let normal = (point - self.center).normalize();
            lighting(&self.lights, point, normal, self.center, self.specular)
        };
        illumination = illumination.clamp(0.0, 1.0);
        let red = c.red() as f32 * illumination;
        let green = c.green() as f32 * illumination;
        let blue = c.blue() as f32 * illumination;
        rgb8(red as u8, green as u8, blue as u8)
    }

    pub fn intersect(&self, direction: Point3) -> Option<(f32, f32)> {
        let w = self.camera - self.center;
        let a = direction.mag2();
        let b = 2.0 * w.dot(direction);
        let c = w.mag2() - self.radius * self.radius;
        let discr = b * b - 4.0 * a * c;
        if discr < 0.0 {
            return None;
        }
        let t1 = (-b + discr.sqrt()) / (2.0 * a);
        let t2 = (-b - discr.sqrt()) / (2.0 * a);
        Some((t1, t2))
    }

    pub fn trace_ray(&self, direction: Point3) -> Option<Color> {
        if let Some((t1, t2)) = self.intersect(direction) {
            let t = t1.min(t2);
            let p = self.camera + direction * t;
            return Some(self.color(p));
        }
        None
    }

    pub fn on_sphere(&self, canvas: &mut Canvas) {
        let cw2 = canvas.width() as i32 / 2;
        let ch2 = canvas.height() as i32 / 2;
        for x in 1 - cw2..cw2 {
            let x32 = x as f32;
            for y in 1 - ch2..ch2 {
                let y32 = y as f32;
                let d = Point3::new(x32, y32, self.focal_len) - self.camera;
                if let Some(c) = self.trace_ray(d) {
                    let p = pt(
                        canvas.width() as f32 / 2.0 + x32,
                        canvas.height() as f32 / 2.0 - y32,
                    );
                    canvas.dot(p.x, p.y, c);
                }
            }
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub enum LightSource {
    Ambient,
    Point,
    Directional,
}

#[derive(Debug, Clone, Copy)]
pub struct Light {
    source: LightSource,
    intensity: f32,
    vector: Point3,
}

impl Light {
    pub fn new(source: LightSource, intensity: f32, vector: Point3) -> Self {
        Self {
            source,
            intensity,
            vector,
        }
    }

    pub fn ambient(intensity: f32) -> Self {
        Self {
            source: LightSource::Ambient,
            intensity,
            vector: Point3::new(0.0, 0.0, 0.0),
        }
    }

    pub fn point(intensity: f32, x: f32, y: f32, z: f32) -> Self {
        let vector = Point3::new(x, y, z);
        Self {
            source: LightSource::Point,
            intensity,
            vector,
        }
    }

    pub fn directional(intensity: f32, x_dir: f32, y_dir: f32, z_dir: f32) -> Self {
        let vector = Point3::new(x_dir, y_dir, z_dir);
        Self {
            source: LightSource::Directional,
            intensity,
            vector,
        }
    }
}

pub fn lighting(
    lights: &[Light],
    point: Point3,
    normal: Point3,
    camera: Point3,
    specular: Option<f32>,
) -> f32 {
    let mut intensity = 0.0;
    for light in lights {
        let light_vec = match light.source {
            LightSource::Ambient => Point3::new(0.0, 0.0, 0.0),
            LightSource::Point => light.vector - point,
            LightSource::Directional => light.vector,
        };
        match light.source {
            LightSource::Ambient => intensity += light.intensity,
            LightSource::Point | LightSource::Directional => {
                let nl = normal.dot(light_vec);
                let lv = light_vec.mag();
                intensity += light.intensity * nl.max(0.0) / lv;
                if let Some(s) = specular {
                    let r = (normal * 2.0 * normal.dot(light_vec)) - light_vec;
                    let rv = r.dot(point - camera);
                    if rv > 0.0 {
                        intensity +=
                            light.intensity * (rv / (r.mag() * (point - camera).mag())).powf(s)
                    }
                };
            }
        }
    }
    intensity
}