wassily_algorithms/
sphere.rs

1//! # 3D Sphere Rendering
2//!
3//! Advanced 3D sphere rendering with realistic lighting, texture mapping, and
4//! perspective projection. This module provides a complete 3D rendering pipeline
5//! optimized for generative art applications.
6//!
7//! ## Key Features
8//!
9//! - **Realistic Lighting**: Multiple light sources with diffuse and specular reflection
10//! - **Texture Mapping**: Apply 2D canvases as textures on 3D spheres
11//! - **3D Rotation**: Rotate spheres around X, Y, and Z axes
12//! - **Perspective Projection**: Realistic perspective with configurable focal length
13//! - **Multiple Lights**: Support for multiple light sources
14//!
15//! ## Basic Usage
16//!
17//! ```no_run
18//! use wassily_algorithms::*;
19//! use wassily_core::*;
20//!
21//! // Create a texture
22//! let mut texture = Canvas::new(256, 256);
23//! texture.fill(*BLUE);
24//!
25//! // Set up a basic scene
26//! let scene = SphereScene::basic(pt3(0.0, 0.0, 100.0), &texture);
27//!
28//! // Render to output
29//! let mut output = Canvas::new(800, 600);
30//! render_sphere(&scene, &mut output);
31//! ```
32//!
33//! ## Advanced Lighting
34//!
35//! ```no_run
36//! use wassily_algorithms::*;
37//! use wassily_core::*;
38//!
39//! let texture = Canvas::new(256, 256);
40//! 
41//! // Create lights
42//! let lights = vec![
43//!     Light::new(pt3(-10.0, 10.0, 5.0), *WHITE, 0.8),
44//!     Light::new(pt3(10.0, -5.0, 10.0), *BLUE, 0.3),
45//! ];
46//!
47//! // Set up scene with custom lighting
48//! let scene = SphereScene::new(
49//!     pt3(0.0, 0.0, 0.0),    // camera position
50//!     400.0,                  // focal length
51//!     pt3(0.0, 0.0, 100.0),  // sphere center
52//!     50.0,                   // sphere radius
53//!     &texture,
54//!     0.1, 0.2, 0.0,         // rotation angles
55//!     lights,
56//!     Some(32.0),            // specular exponent
57//! );
58//! ```
59//!
60//! ## Components
61//!
62//! - [`SphereScene`]: Complete scene configuration
63//! - [`Light`]: Light source definition
64//! - Main rendering function for sphere scenes
65
66use wassily_core::{canvas::Canvas, points::*};
67use wassily_color::rgb8;
68use std::f32::consts::PI;
69use tiny_skia::Color;
70
71/// Configuration for a 3D sphere rendering scene.
72/// 
73/// This struct contains all the parameters needed to render a textured sphere
74/// with realistic lighting and perspective projection.
75/// 
76/// # Fields
77/// 
78/// - `camera`: Position of the camera in 3D space
79/// - `focal_len`: Focal length for perspective projection (higher = more telephoto)
80/// - `center`: Position of the sphere center in 3D space
81/// - `radius`: Radius of the sphere
82/// - `texture`: 2D canvas to use as texture on the sphere surface
83/// - `rotation_x`, `rotation_y`, `rotation_z`: Rotation angles in radians
84/// - `lights`: Vector of light sources
85/// - `specular`: Optional specular reflection exponent (higher = shinier)
86pub struct SphereScene<'a> {
87    pub camera: Point3,
88    pub focal_len: f32,
89    pub center: Point3,
90    pub radius: f32,
91    pub texture: &'a Canvas,
92    pub rotation_x: f32,
93    pub rotation_y: f32,
94    pub rotation_z: f32,
95    pub lights: Vec<Light>,
96    pub specular: Option<f32>,
97}
98
99impl<'a> SphereScene<'a> {
100    pub fn new(
101        camera: Point3,
102        focal_len: f32,
103        center: Point3,
104        radius: f32,
105        texture: &'a Canvas,
106        rotation_x: f32,
107        rotation_y: f32,
108        rotation_z: f32,
109        lights: Vec<Light>,
110        specular: Option<f32>,
111    ) -> Self {
112        Self {
113            camera,
114            focal_len,
115            center,
116            radius,
117            texture,
118            rotation_x,
119            rotation_y,
120            rotation_z,
121            lights,
122            specular,
123        }
124    }
125
126    pub fn basic(center: Point3, texture: &'a Canvas) -> Self {
127        Self {
128            camera: Point3::new(0.0, 0.0, 0.0),
129            focal_len: texture.width() as f32,
130            center,
131            radius: texture.width() as f32 / 2.0,
132            texture,
133            rotation_x: PI / 2.0,
134            rotation_y: 0.0,
135            rotation_z: 0.0,
136            lights: vec![],
137            specular: None,
138        }
139    }
140
141    pub fn color(&self, point: Point3) -> Color {
142        let w = 2.0 * self.texture.width() as f32;
143        let h = self.texture.height() as f32;
144        let rot_point = (point - self.center)
145            .rotate_x(self.rotation_x)
146            .rotate_y(self.rotation_y)
147            .rotate_z(self.rotation_z)
148            + self.center;
149        let s = rot_point.to_spherical(self.center);
150        let u = w / 2.0 * (s.phi / PI + 1.0);
151        let v = h * (1.0 - s.theta / PI);
152        let c = self.texture.pixmap.pixel(u as u32, v as u32).unwrap();
153        let mut illumination = if self.lights.is_empty() {
154            1.0
155        } else {
156            let normal = (point - self.center).normalize();
157            lighting(&self.lights, point, normal, self.center, self.specular)
158        };
159        illumination = illumination.clamp(0.0, 1.0);
160        let red = c.red() as f32 * illumination;
161        let green = c.green() as f32 * illumination;
162        let blue = c.blue() as f32 * illumination;
163        rgb8(red as u8, green as u8, blue as u8)
164    }
165
166    pub fn intersect(&self, direction: Point3) -> Option<(f32, f32)> {
167        let w = self.camera - self.center;
168        let a = direction.mag2();
169        let b = 2.0 * w.dot(direction);
170        let c = w.mag2() - self.radius * self.radius;
171        let discr = b * b - 4.0 * a * c;
172        if discr < 0.0 {
173            return None;
174        }
175        let t1 = (-b + discr.sqrt()) / (2.0 * a);
176        let t2 = (-b - discr.sqrt()) / (2.0 * a);
177        Some((t1, t2))
178    }
179
180    pub fn trace_ray(&self, direction: Point3) -> Option<Color> {
181        if let Some((t1, t2)) = self.intersect(direction) {
182            let t = t1.min(t2);
183            let p = self.camera + direction * t;
184            return Some(self.color(p));
185        }
186        None
187    }
188
189    pub fn on_sphere(&self, canvas: &mut Canvas) {
190        let cw2 = canvas.width() as i32 / 2;
191        let ch2 = canvas.height() as i32 / 2;
192        for x in 1 - cw2..cw2 {
193            let x32 = x as f32;
194            for y in 1 - ch2..ch2 {
195                let y32 = y as f32;
196                let d = Point3::new(x32, y32, self.focal_len) - self.camera;
197                if let Some(c) = self.trace_ray(d) {
198                    let p = pt(
199                        canvas.width() as f32 / 2.0 + x32,
200                        canvas.height() as f32 / 2.0 - y32,
201                    );
202                    canvas.dot(p.x, p.y, c);
203                }
204            }
205        }
206    }
207}
208
209#[derive(Debug, Clone, Copy)]
210pub enum LightSource {
211    Ambient,
212    Point,
213    Directional,
214}
215
216#[derive(Debug, Clone, Copy)]
217pub struct Light {
218    source: LightSource,
219    intensity: f32,
220    vector: Point3,
221}
222
223impl Light {
224    pub fn new(source: LightSource, intensity: f32, vector: Point3) -> Self {
225        Self {
226            source,
227            intensity,
228            vector,
229        }
230    }
231
232    pub fn ambient(intensity: f32) -> Self {
233        Self {
234            source: LightSource::Ambient,
235            intensity,
236            vector: Point3::new(0.0, 0.0, 0.0),
237        }
238    }
239
240    pub fn point(intensity: f32, x: f32, y: f32, z: f32) -> Self {
241        let vector = Point3::new(x, y, z);
242        Self {
243            source: LightSource::Point,
244            intensity,
245            vector,
246        }
247    }
248
249    pub fn directional(intensity: f32, x_dir: f32, y_dir: f32, z_dir: f32) -> Self {
250        let vector = Point3::new(x_dir, y_dir, z_dir);
251        Self {
252            source: LightSource::Directional,
253            intensity,
254            vector,
255        }
256    }
257}
258
259pub fn lighting(
260    lights: &[Light],
261    point: Point3,
262    normal: Point3,
263    camera: Point3,
264    specular: Option<f32>,
265) -> f32 {
266    let mut intensity = 0.0;
267    for light in lights {
268        let light_vec = match light.source {
269            LightSource::Ambient => Point3::new(0.0, 0.0, 0.0),
270            LightSource::Point => light.vector - point,
271            LightSource::Directional => light.vector,
272        };
273        match light.source {
274            LightSource::Ambient => intensity += light.intensity,
275            LightSource::Point | LightSource::Directional => {
276                let nl = normal.dot(light_vec);
277                let lv = light_vec.mag();
278                intensity += light.intensity * nl.max(0.0) / lv;
279                if let Some(s) = specular {
280                    let r = (normal * 2.0 * normal.dot(light_vec)) - light_vec;
281                    let rv = r.dot(point - camera);
282                    if rv > 0.0 {
283                        intensity +=
284                            light.intensity * (rv / (r.mag() * (point - camera).mag())).powf(s)
285                    }
286                };
287            }
288        }
289    }
290    intensity
291}