psychopath/src/shading/surface_closure.rs
Nathan Vegdahl 0481d931b9 Surface closures and light sampling now take both shading and geometric normals.
This will allow them to appropriately handle weirdness that comes
from the shading normal and geometric normal being different.
2017-07-30 19:17:32 -07:00

696 lines
21 KiB
Rust

#![allow(dead_code)]
use std::f32::consts::PI as PI_32;
use color::{XYZ, SpectralSample, Color};
use math::{Vector, Normal, dot, clamp, zup_to_vec};
use sampling::cosine_sample_hemisphere;
use lerp::lerp;
const INV_PI: f32 = 1.0 / PI_32;
const H_PI: f32 = PI_32 / 2.0;
#[derive(Debug, Copy, Clone)]
pub enum SurfaceClosureUnion {
EmitClosure(EmitClosure),
LambertClosure(LambertClosure),
GTRClosure(GTRClosure),
}
impl SurfaceClosureUnion {
pub fn as_surface_closure(&self) -> &SurfaceClosure {
match *self {
SurfaceClosureUnion::EmitClosure(ref closure) => closure as &SurfaceClosure,
SurfaceClosureUnion::LambertClosure(ref closure) => closure as &SurfaceClosure,
SurfaceClosureUnion::GTRClosure(ref closure) => closure as &SurfaceClosure,
}
}
}
/// Trait for surface closures.
pub trait SurfaceClosure {
/// Returns whether the closure has a delta distribution or not.
fn is_delta(&self) -> bool;
/// Given an incoming ray and sample values, generates an outgoing ray and
/// color filter.
///
/// inc: Incoming light direction.
/// nor: The shading surface normal at the surface point.
/// nor_g: The geometric surface normal at the surface point.
/// uv: The sampling values.
/// wavelength: The wavelength of light to sample at.
///
/// Returns a tuple with the generated outgoing light direction, color filter, and pdf.
fn sample(
&self,
inc: Vector,
nor: Normal,
nor_g: Normal,
uv: (f32, f32),
wavelength: f32,
) -> (Vector, SpectralSample, f32);
/// Evaluates the closure for the given incoming and outgoing rays.
///
/// inc: The incoming light direction.
/// out: The outgoing light direction.
/// nor: The shading surface normal at the surface point.
/// nor_g: The geometric surface normal at the surface point.
/// wavelength: The wavelength of light to evaluate for.
///
/// Returns the resulting filter color.
fn evaluate(
&self,
inc: Vector,
out: Vector,
nor: Normal,
nor_g: Normal,
wavelength: f32,
) -> SpectralSample;
/// Returns the pdf for the given 'in' direction producing the given 'out'
/// direction with the given differential geometry.
///
/// inc: The incoming light direction.
/// out: The outgoing light direction.
/// nor: The shading surface normal at the surface point.
/// nor_g: The geometric surface normal at the surface point.
fn sample_pdf(&self, inc: Vector, out: Vector, nor: Normal, nor_g: Normal) -> f32;
/// Returns an estimate of the sum total energy that evaluate() would return
/// when 'out' is evaluated over a circular solid angle.
/// This is used for importance sampling, so does not need to be exact,
/// but it does need to be non-zero anywhere that an exact solution would
/// be non-zero.
fn estimate_eval_over_solid_angle(
&self,
inc: Vector,
out: Vector,
nor: Normal,
nor_g: Normal,
cos_theta: f32,
) -> f32;
}
/// Utility function that calculates the fresnel reflection factor of a given
/// incoming ray against a surface with the given ior outside/inside ratio.
///
/// `ior_ratio`: The ratio of the outside material ior (probably 1.0 for air)
/// over the inside ior.
/// `c`: The cosine of the angle between the incoming light and the
/// surface's normal. Probably calculated e.g. with a normalized
/// dot product.
#[allow(dead_code)]
fn dielectric_fresnel(ior_ratio: f32, c: f32) -> f32 {
let g = (ior_ratio - 1.0 + (c * c)).sqrt();
let f1 = g - c;
let f2 = g + c;
let f3 = (f1 * f1) / (f2 * f2);
let f4 = (c * f2) - 1.0;
let f5 = (c * f1) + 1.0;
let f6 = 1.0 + ((f4 * f4) / (f5 * f5));
0.5 * f3 * f6
}
/// Schlick's approximation of the fresnel reflection factor.
///
/// Same interface as `dielectric_fresnel()`, above.
#[allow(dead_code)]
fn schlick_fresnel(ior_ratio: f32, c: f32) -> f32 {
let f1 = (1.0 - ior_ratio) / (1.0 + ior_ratio);
let f2 = f1 * f1;
let c1 = 1.0 - c;
let c2 = c1 * c1;
f2 + ((1.0 - f2) * c1 * c2 * c2)
}
/// Utility function that calculates the fresnel reflection factor of a given
/// incoming ray against a surface with the given normal-reflectance factor.
///
/// `frensel_fac`: The ratio of light reflected back if the ray were to
/// hit the surface head-on (perpendicular to the surface).
/// `c`: The cosine of the angle between the incoming light and the
/// surface's normal. Probably calculated e.g. with a normalized
/// dot product.
#[allow(dead_code)]
fn dielectric_fresnel_from_fac(fresnel_fac: f32, c: f32) -> f32 {
let tmp1 = fresnel_fac.sqrt() - 1.0;
// Protect against divide by zero.
if tmp1.abs() < 0.000001 {
return 1.0;
}
// Find the ior ratio
let tmp2 = (-2.0 / tmp1) - 1.0;
let ior_ratio = tmp2 * tmp2;
// Calculate fresnel factor
dielectric_fresnel(ior_ratio, c)
}
/// Schlick's approximation version of `dielectric_fresnel_from_fac()` above.
#[allow(dead_code)]
fn schlick_fresnel_from_fac(frensel_fac: f32, c: f32) -> f32 {
let c1 = 1.0 - c;
let c2 = c1 * c1;
frensel_fac + ((1.0 - frensel_fac) * c1 * c2 * c2)
}
/// Emit closure.
///
/// NOTE: this needs to be handled specially by the integrator! It does not
/// behave like a standard closure!
#[derive(Debug, Copy, Clone)]
pub struct EmitClosure {
col: XYZ,
}
impl EmitClosure {
pub fn emitted_color(&self, wavelength: f32) -> SpectralSample {
self.col.to_spectral_sample(wavelength)
}
}
impl SurfaceClosure for EmitClosure {
fn is_delta(&self) -> bool {
false
}
fn sample(
&self,
inc: Vector,
nor: Normal,
nor_g: Normal,
uv: (f32, f32),
wavelength: f32,
) -> (Vector, SpectralSample, f32) {
let _ = (inc, nor, nor_g, uv); // Not using these, silence warning
(
Vector::new(0.0, 0.0, 0.0),
SpectralSample::new(wavelength),
1.0,
)
}
fn evaluate(
&self,
inc: Vector,
out: Vector,
nor: Normal,
nor_g: Normal,
wavelength: f32,
) -> SpectralSample {
let _ = (inc, out, nor, nor_g); // Not using these, silence warning
SpectralSample::new(wavelength)
}
fn sample_pdf(&self, inc: Vector, out: Vector, nor: Normal, nor_g: Normal) -> f32 {
let _ = (inc, out, nor, nor_g); // Not using these, silence warning
1.0
}
fn estimate_eval_over_solid_angle(
&self,
inc: Vector,
out: Vector,
nor: Normal,
nor_g: Normal,
cos_theta: f32,
) -> f32 {
let _ = (inc, out, nor, nor_g, cos_theta); // Not using these, silence warning
// TODO: what to do here?
unimplemented!()
}
}
/// Lambertian surface closure
#[derive(Debug, Copy, Clone)]
pub struct LambertClosure {
col: XYZ,
}
impl LambertClosure {
pub fn new(col: XYZ) -> LambertClosure {
LambertClosure { col: col }
}
}
impl SurfaceClosure for LambertClosure {
fn is_delta(&self) -> bool {
false
}
fn sample(
&self,
inc: Vector,
nor: Normal,
nor_g: Normal,
uv: (f32, f32),
wavelength: f32,
) -> (Vector, SpectralSample, f32) {
let nn = if dot(nor_g.into_vector(), inc) <= 0.0 {
nor.normalized()
} else {
-nor.normalized()
}.into_vector();
// Generate a random ray direction in the hemisphere
// of the surface.
let dir = cosine_sample_hemisphere(uv.0, uv.1);
let pdf = dir.z() * INV_PI;
let out = zup_to_vec(dir, nn);
let filter = self.evaluate(inc, out, nor, nor_g, wavelength);
(out, filter, pdf)
}
fn evaluate(
&self,
inc: Vector,
out: Vector,
nor: Normal,
nor_g: Normal,
wavelength: f32,
) -> SpectralSample {
let v = out.normalized();
let nn = if dot(nor_g.into_vector(), inc) <= 0.0 {
nor.normalized()
} else {
-nor.normalized()
}.into_vector();
let fac = dot(nn, v).max(0.0) * INV_PI;
self.col.to_spectral_sample(wavelength) * fac
}
fn sample_pdf(&self, inc: Vector, out: Vector, nor: Normal, nor_g: Normal) -> f32 {
let v = out.normalized();
let nn = if dot(nor_g.into_vector(), inc) <= 0.0 {
nor.normalized()
} else {
-nor.normalized()
}.into_vector();
dot(nn, v).max(0.0) * INV_PI
}
fn estimate_eval_over_solid_angle(
&self,
inc: Vector,
out: Vector,
nor: Normal,
nor_g: Normal,
cos_theta: f32,
) -> f32 {
let _ = nor_g; // Not using this, silence warning
assert!(cos_theta >= -1.0 && cos_theta <= 1.0);
// Analytically calculates lambert shading from a uniform light source
// subtending a circular solid angle.
// Only works for solid angle subtending equal to or less than a hemisphere.
//
// Formula taken from "Area Light Sources for Real-Time Graphics"
// by John M. Snyder
fn sphere_lambert(nlcos: f32, rcos: f32) -> f32 {
assert!(nlcos >= -1.0 && nlcos <= 1.0);
assert!(rcos >= 0.0 && rcos <= 1.0);
let nlsin: f32 = (1.0 - (nlcos * nlcos)).sqrt();
let rsin2: f32 = 1.0 - (rcos * rcos);
let rsin: f32 = rsin2.sqrt();
let ysin: f32 = rcos / nlsin;
let ycos2: f32 = 1.0 - (ysin * ysin);
let ycos: f32 = ycos2.sqrt();
let g: f32 = (-2.0 * nlsin * rcos * ycos) + H_PI - ysin.asin() + (ysin * ycos);
let h: f32 = nlcos * ((ycos * (rsin2 - ycos2).sqrt()) + (rsin2 * (ycos / rsin).asin()));
let nl: f32 = nlcos.acos();
let r: f32 = rcos.acos();
if nl < (H_PI - r) {
nlcos * rsin2
} else if nl < H_PI {
(nlcos * rsin2) + g - h
} else if nl < (H_PI + r) {
(g + h) * INV_PI
} else {
0.0
}
}
if cos_theta < 0.0 {
return 1.0;
} else {
let v = out.normalized();
let nn = if dot(nor.into_vector(), inc) <= 0.0 {
nor.normalized()
} else {
-nor.normalized()
}.into_vector();
let cos_nv = dot(nn, v).max(-1.0).min(1.0);
return sphere_lambert(cos_nv, cos_theta);
}
}
}
/// The GTR microfacet BRDF from the Disney Principled BRDF paper.
#[derive(Debug, Copy, Clone)]
pub struct GTRClosure {
col: XYZ,
roughness: f32,
tail_shape: f32,
fresnel: f32, // [0.0, 1.0] determines how much fresnel reflection comes into play
normalization_factor: f32,
}
impl GTRClosure {
pub fn new(col: XYZ, roughness: f32, tail_shape: f32, fresnel: f32) -> GTRClosure {
let mut closure = GTRClosure {
col: col,
roughness: roughness,
tail_shape: tail_shape,
fresnel: fresnel,
normalization_factor: GTRClosure::normalization(roughness, tail_shape),
};
closure.validate();
closure
}
// Returns the normalization factor for the distribution function
// of the BRDF.
fn normalization(r: f32, t: f32) -> f32 {
let r2 = r * r;
let top = (t - 1.0) * (r2 - 1.0);
let bottom = PI_32 * (1.0 - r2.powf(1.0 - t));
top / bottom
}
// Makes sure values are in a valid range
fn validate(&mut self) {
debug_assert!(self.fresnel >= 0.0 && self.fresnel <= 1.0);
// Clamp values to valid ranges
self.roughness = clamp(self.roughness, 0.0, 0.9999);
self.tail_shape = (0.0001f32).max(self.tail_shape);
// When roughness is too small, but not zero, there are floating point accuracy issues
if self.roughness < 0.000244140625 {
// (2^-12)
self.roughness = 0.0;
}
// If tail_shape is too near 1.0, push it away a tiny bit.
// This avoids having to have a special form of various equations
// due to a singularity at tail_shape = 1.0
// That in turn avoids some branches in the code, and the effect of
// tail_shape is sufficiently subtle that there is no visible
// difference in renders.
const TAIL_EPSILON: f32 = 0.0001;
if (self.tail_shape - 1.0).abs() < TAIL_EPSILON {
self.tail_shape = 1.0 + TAIL_EPSILON;
}
// Precalculate normalization factor
self.normalization_factor = GTRClosure::normalization(self.roughness, self.tail_shape);
}
// Returns the cosine of the half-angle that should be sampled, given
// a random variable in [0,1]
fn half_theta_sample(&self, u: f32) -> f32 {
let roughness2 = self.roughness * self.roughness;
// Calculate top half of equation
let top = 1.0 -
((roughness2.powf(1.0 - self.tail_shape) * (1.0 - u)) + u)
.powf(1.0 / (1.0 - self.tail_shape));
// Calculate bottom half of equation
let bottom = 1.0 - roughness2;
(top / bottom).sqrt()
}
/// Microfacet distribution function.
///
/// nh: cosine of the angle between the surface normal and the microfacet normal.
fn dist(&self, nh: f32, rough: f32) -> f32 {
// Other useful numbers
let roughness2 = rough * rough;
// Calculate D - Distribution
if nh <= 0.0 {
0.0
} else {
let nh2 = nh * nh;
self.normalization_factor / (1.0 + ((roughness2 - 1.0) * nh2)).powf(self.tail_shape)
}
}
}
impl SurfaceClosure for GTRClosure {
fn is_delta(&self) -> bool {
self.roughness == 0.0
}
fn sample(
&self,
inc: Vector,
nor: Normal,
nor_g: Normal,
uv: (f32, f32),
wavelength: f32,
) -> (Vector, SpectralSample, f32) {
// Get normalized surface normal
let nn = if dot(nor_g.into_vector(), inc) < 0.0 {
nor.normalized()
} else {
-nor.normalized() // If back-facing, flip normal
}.into_vector();
// Generate a random ray direction in the hemisphere
// of the surface.
let theta_cos = self.half_theta_sample(uv.0);
let theta_sin = (1.0 - (theta_cos * theta_cos)).sqrt();
let angle = uv.1 * PI_32 * 2.0;
let mut half_dir = Vector::new(angle.cos() * theta_sin, angle.sin() * theta_sin, theta_cos);
half_dir = zup_to_vec(half_dir, nn).normalized();
let out = inc - (half_dir * 2.0 * dot(inc, half_dir));
let filter = self.evaluate(inc, out, nor, nor_g, wavelength);
let pdf = self.sample_pdf(inc, out, nor, nor_g);
(out, filter, pdf)
}
fn evaluate(
&self,
inc: Vector,
out: Vector,
nor: Normal,
nor_g: Normal,
wavelength: f32,
) -> SpectralSample {
// Calculate needed vectors, normalized
let aa = -inc.normalized(); // Vector pointing to where "in" came from
let bb = out.normalized(); // Out
let hh = (aa + bb).normalized(); // Half-way between aa and bb
// Surface normal
let nn = if dot(nor_g.into_vector(), hh) < 0.0 {
-nor.normalized() // If back-facing, flip normal
} else {
nor.normalized()
}.into_vector();
if dot(nn, aa) < 0.0 {
return SpectralSample::from_value(0.0, 0.0);
}
// Calculate needed dot products
let na = clamp(dot(nn, aa), -1.0, 1.0);
let nb = clamp(dot(nn, bb), -1.0, 1.0);
let ha = clamp(dot(hh, aa), -1.0, 1.0);
let hb = clamp(dot(hh, bb), -1.0, 1.0);
let nh = clamp(dot(nn, hh), -1.0, 1.0);
// Other useful numbers
let roughness2 = self.roughness * self.roughness;
// Calculate F - Fresnel
let col_f = {
let mut col_f = self.col.to_spectral_sample(wavelength);
let rev_fresnel = 1.0 - self.fresnel;
let c0 = lerp(
schlick_fresnel_from_fac(col_f.e.get_0(), hb),
col_f.e.get_0(),
rev_fresnel,
);
let c1 = lerp(
schlick_fresnel_from_fac(col_f.e.get_1(), hb),
col_f.e.get_1(),
rev_fresnel,
);
let c2 = lerp(
schlick_fresnel_from_fac(col_f.e.get_2(), hb),
col_f.e.get_2(),
rev_fresnel,
);
let c3 = lerp(
schlick_fresnel_from_fac(col_f.e.get_3(), hb),
col_f.e.get_3(),
rev_fresnel,
);
col_f.e.set_0(c0);
col_f.e.set_1(c1);
col_f.e.set_2(c2);
col_f.e.set_3(c3);
col_f
};
// Calculate everything else
if self.roughness == 0.0 {
// If sharp mirror, just return col * fresnel factor
return col_f;
} else {
// Calculate D - Distribution
let dist = if nh > 0.0 {
let nh2 = nh * nh;
self.normalization_factor / (1.0 + ((roughness2 - 1.0) * nh2)).powf(self.tail_shape)
} else {
0.0
};
// Calculate G1 - Geometric microfacet shadowing
let g1 = {
let na2 = na * na;
let tan_na = ((1.0 - na2) / na2).sqrt();
let g1_pos_char = if (ha * na) > 0.0 { 1.0 } else { 0.0 };
let g1_a = roughness2 * tan_na;
let g1_b = ((1.0 + (g1_a * g1_a)).sqrt() - 1.0) * 0.5;
g1_pos_char / (1.0 + g1_b)
};
// Calculate G2 - Geometric microfacet shadowing
let g2 = {
let nb2 = nb * nb;
let tan_nb = ((1.0 - nb2) / nb2).sqrt();
let g2_pos_char = if (hb * nb) > 0.0 { 1.0 } else { 0.0 };
let g2_a = roughness2 * tan_nb;
let g2_b = ((1.0 + (g2_a * g2_a)).sqrt() - 1.0) * 0.5;
g2_pos_char / (1.0 + g2_b)
};
// Final result
col_f * (dist * g1 * g2) * INV_PI
}
}
fn sample_pdf(&self, inc: Vector, out: Vector, nor: Normal, nor_g: Normal) -> f32 {
// Calculate needed vectors, normalized
let aa = -inc.normalized(); // Vector pointing to where "in" came from
let bb = out.normalized(); // Out
let hh = (aa + bb).normalized(); // Half-way between aa and bb
// Surface normal
let nn = if dot(nor_g.into_vector(), hh) < 0.0 {
-nor.normalized() // If back-facing, flip normal
} else {
nor.normalized()
}.into_vector();
if dot(nn, aa) < 0.0 {
return 0.0;
}
// Calculate needed dot products
let nh = clamp(dot(nn, hh), -1.0, 1.0);
self.dist(nh, self.roughness) * INV_PI
}
fn estimate_eval_over_solid_angle(
&self,
inc: Vector,
out: Vector,
nor: Normal,
nor_g: Normal,
cos_theta: f32,
) -> f32 {
// TODO: all of the stuff in this function is horribly hacky.
// Find a proper way to approximate the light contribution from a
// solid angle.
let _ = nor_g; // Not using this, silence warning
assert!(cos_theta >= -1.0);
assert!(cos_theta <= 1.0);
// Surface normal
let nn = if dot(nor.into_vector(), inc) < 0.0 {
nor.normalized()
} else {
-nor.normalized() // If back-facing, flip normal
}.into_vector();
let aa = -inc.normalized(); // Vector pointing to where "in" came from
let bb = out.normalized(); // Out
// Brute-force method
//let mut fac = 0.0;
//const N: usize = 256;
//for i in 0..N {
// let uu = Halton::sample(0, i);
// let vv = Halton::sample(1, i);
// let mut samp = uniform_sample_cone(uu, vv, cos_theta);
// samp = zup_to_vec(samp, bb).normalized();
// if dot(nn, samp) > 0.0 {
// let hh = (aa+samp).normalized();
// fac += self.dist(dot(nn, hh), roughness);
// }
//}
//fac /= N * N;
// Approximate method
let theta = cos_theta.acos();
let hh = (aa + bb).normalized();
let nh = clamp(dot(nn, hh), -1.0, 1.0);
let fac = self.dist(
nh,
(1.0f32).min(self.roughness.sqrt() + (2.0 * theta / PI_32)),
);
fac * (1.0f32).min(1.0 - cos_theta) * INV_PI
}
}