rustic_zen/material.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Materials in Rustic Zen are defined as closures that act on a single ray when intersecting wtth an object.
6//!
7//! For object safety these closures are stored in a `std::sync::Arc` which can then be cloned to as many users as needed.
8//! As there is a constant generic type assosiated with the material, `R` it is usually easiest to implement the Arc inside a
9//! generator function that yeilds the Arc and accepts a type argument.
10//!
11//! # Examples:
12//! ## Create a trivial shader and assign it to several `Segments`
13//! ```
14//! use rustic_zen::prelude::*;
15//! use std::sync::Arc;
16//!
17//! // This shader will fire all incoming rays in the direction of the surface normal.
18//! // This is actually surprisingly useful for testing normals.
19//! fn always_normal<R>() -> material::Material<R> {
20//! Arc::new(move |_, normal: &Vector, _, _, _: &mut R| {
21//! Some(normal.clone())
22//! })
23//! }
24//!
25//! let m = always_normal(); // R fixed to a concrete type here by the `Scene` below
26//!
27//! let s = Segment::line_from_points((0.0, 0.0), (10.0, 10.0), m.clone());
28//! let s2 = Segment::line_from_points((10.0, 10.0), (20.0, 0.0), m);
29//!
30//! // you may get unknown type for <R> if you do not add all your segments to a scene.
31//! let _ = Scene::new(100, 100).with_object(s).with_object(s2);
32//! ```
33//!
34//! ## Create a more useful shader that refects 50% of incoming light and apply it to several `Segments`:
35//! ```
36//! use rustic_zen::prelude::*;
37//! use std::sync::Arc;
38//!
39//! // This shader will reflect half of the rays and absorb the other half
40//! fn mirror<R: Rng + 'static>() -> material::Material<R> {
41//! let s = Sampler::<f64, R>::new_range(0.0, 1.0);
42//! Arc::new(move |direction: &Vector, normal: &Vector, _, _, rng: &mut R| {
43//! if s.sample(rng) < 0.5 {
44//! None
45//! } else {
46//! Some(direction.reflect(normal))
47//! }
48//! })
49//! }
50//!
51//! let m = mirror(); // R fixed to a concrete type here by the `Scene` below
52//!
53//! let s = Segment::line_from_points((0.0, 0.0), (10.0, 10.0), m.clone());
54//! let s2 = Segment::line_from_points((10.0, 10.0), (20.0, 0.0), m);
55//!
56//! // you may get unknown type for <R> if you do not add all your segments to a scene.
57//! let _ = Scene::new(100, 100).with_object(s).with_object(s2);
58//! ```
59//!
60//! Admittedly this could probably be less clunky.
61//!
62//! Creating more imaginative shaders is left as an exersize for the reader.
63
64use crate::geom::Vector;
65use rand::prelude::*;
66
67use std::f64::consts::PI;
68use std::sync::Arc;
69
70/// Shader Type
71///
72/// This Closure Represents a shader in the HQZ system. For a given incident ray an outcome is calculated.
73///
74/// Wavelength is provided instead of colour to encourage the design of physically based
75/// shaders. Colour can be calculated from the wavelength using Rustic's spectrum module.
76///
77/// * It returns a wrapped vector of the direction of the bounced ray.
78/// If the ray is absorbed then it returns `None`.
79///
80/// # Parameters:
81/// - __direction__: Vector of the direction of the inbound ray.
82/// - __normal__: Computed normal to the hit surface. This can be used in `direction.reflect(normal)`, to get a mirror reflection.
83/// - __wavelength__: Wavelength of inbound ray (no way to change this for the outbound ray, sorry).
84/// - __alpha__: how far along the object the inbound ray hit. clamped 0.0 to 1.0
85/// - __rng__: random number generator for use during the function, (don't spawn your own, way to slow.)
86
87pub type Material<R> =
88 Arc<dyn Fn(&Vector, &Vector, f64, f64, &mut R) -> Option<Vector> + Send + Sync + 'static>;
89
90/// Reference / Legacy implementation of Material trait.
91///
92/// This implementation models the behavour of shaders from the original HQZ.
93/// When called this will return an `Arc` containing the shader closure. It only has to be called
94/// once for each set of parameters desired as the arc can be cloned to multiple objects.
95///
96/// # Parameters:
97/// * __diffuse__: proportion of rays bounced diffusely (completely random direction)
98/// * __relective__: proportion of rays that are perfectly reflected
99/// * __transparent__: proportion of rays that pass through the material as if it was transparent.
100///
101/// The proportion of rays absorbed is `1.0 - d - r - t`
102///
103/// # Panics:
104/// Panics if `d + r + t > 1.0`
105pub fn hqz_legacy<R: Rng>(diffuse: f64, reflective: f64, transparent: f64) -> Material<R> {
106 if diffuse + reflective + transparent > 1.0 {
107 panic!("HQZ Legacy shader cooefficents > 1.0");
108 }
109
110 Arc::new(
111 move |direction: &Vector, normal: &Vector, _wavelength: f64, _alpha: f64, rng: &mut R| {
112 let f: f64 = rng.gen_range(0.0..1.0);
113
114 if f <= diffuse {
115 let angle = rng.gen_range(0.0..2.0 * PI);
116 return Some(Vector {
117 x: f64::cos(angle),
118 y: f64::sin(angle),
119 });
120 }
121
122 if f <= diffuse + reflective {
123 let angle = direction.reflect(normal);
124 return Some(angle);
125 }
126
127 if f <= diffuse + reflective + transparent {
128 let angle = direction.clone();
129 return Some(angle);
130 }
131
132 None
133 },
134 )
135}
136
137/// Default Settings for `hqz_legacy`, makes a fast and predictable testing shader where needed.
138///
139/// Default Values:
140/// * __diffuse__: 0.1
141/// * __reflective__: 0.4
142/// * __transparent__: 0.4
143/// * absorbsion is 0.1
144pub fn hqz_legacy_default<R: Rng>() -> Material<R> {
145 hqz_legacy(0.1, 0.4, 0.4)
146}