psychopath/src/sampling/monte_carlo.rs

201 lines
6.5 KiB
Rust

#![allow(dead_code)]
use std::{f32::consts::FRAC_PI_4 as QPI_32, f32::consts::PI as PI_32, f64::consts::PI as PI_64};
use crate::math::{cross, dot, Point, Vector};
/// Maps the unit square to the unit circle.
/// NOTE: x and y should be distributed within [-1, 1],
/// not [0, 1].
pub fn square_to_circle(x: f32, y: f32) -> (f32, f32) {
debug_assert!(x >= -1.0 && x <= 1.0 && y >= -1.0 && y <= 1.0);
if x == 0.0 && y == 0.0 {
return (0.0, 0.0);
}
let (radius, angle) = if x > y.abs() {
// Quadrant 1
(x, QPI_32 * (y / x))
} else if y > x.abs() {
// Quadrant 2
(y, QPI_32 * (2.0 - (x / y)))
} else if x < -(y.abs()) {
// Quadrant 3
(-x, QPI_32 * (4.0 + (y / x)))
} else {
// Quadrant 4
(-y, QPI_32 * (6.0 - (x / y)))
};
(radius * angle.cos(), radius * angle.sin())
}
pub fn cosine_sample_hemisphere(u: f32, v: f32) -> Vector {
let (u, v) = square_to_circle((u * 2.0) - 1.0, (v * 2.0) - 1.0);
let z = (1.0 - ((u * u) + (v * v))).max(0.0).sqrt();
Vector::new(u, v, z)
}
pub fn uniform_sample_hemisphere(u: f32, v: f32) -> Vector {
let z = u;
let r = (1.0 - (z * z)).max(0.0).sqrt();
let phi = 2.0 * PI_32 * v;
let x = r * phi.cos();
let y = r * phi.sin();
Vector::new(x, y, z)
}
pub fn uniform_sample_sphere(u: f32, v: f32) -> Vector {
let z = 1.0 - (2.0 * u);
let r = (1.0 - (z * z)).max(0.0).sqrt();
let phi = 2.0 * PI_32 * v;
let x = r * phi.cos();
let y = r * phi.sin();
Vector::new(x, y, z)
}
/// Samples a solid angle defined by a cone originating from (0,0,0)
/// and pointing down the positive z-axis.
///
/// `u`, `v`: sampling variables, should each be in the interval [0,1]
/// `cos_theta_max`: cosine of the max angle from the z-axis, defining
/// the outer extent of the cone.
pub fn uniform_sample_cone(u: f32, v: f32, cos_theta_max: f64) -> Vector {
let cos_theta = (1.0 - u as f64) + (u as f64 * cos_theta_max);
let sin_theta = (1.0 - (cos_theta * cos_theta)).sqrt();
let phi = v as f64 * 2.0 * PI_64;
Vector::new(
(phi.cos() * sin_theta) as f32,
(phi.sin() * sin_theta) as f32,
cos_theta as f32,
)
}
pub fn uniform_sample_cone_pdf(cos_theta_max: f64) -> f64 {
// 1.0 / solid angle
1.0 / (2.0 * PI_64 * (1.0 - cos_theta_max))
}
/// Generates a uniform sample on a triangle given two uniform random
/// variables i and j in [0, 1].
pub fn uniform_sample_triangle(va: Vector, vb: Vector, vc: Vector, i: f32, j: f32) -> Vector {
let isqrt = i.sqrt();
let a = 1.0 - isqrt;
let b = isqrt * (1.0 - j);
let c = j * isqrt;
(va * a) + (vb * b) + (vc * c)
}
/// Calculates the surface area of a triangle.
pub fn triangle_surface_area(p0: Point, p1: Point, p2: Point) -> f32 {
0.5 * cross(p1 - p0, p2 - p0).length()
}
/// Calculates the projected solid angle of a spherical triangle.
///
/// A, B, and C are the points of the triangle on a unit sphere.
pub fn spherical_triangle_solid_angle(va: Vector, vb: Vector, vc: Vector) -> f32 {
// Calculate sines and cosines of the spherical triangle's edge lengths
let cos_a: f64 = dot(vb, vc).max(-1.0).min(1.0) as f64;
let cos_b: f64 = dot(vc, va).max(-1.0).min(1.0) as f64;
let cos_c: f64 = dot(va, vb).max(-1.0).min(1.0) as f64;
let sin_a: f64 = (1.0 - (cos_a * cos_a)).sqrt();
let sin_b: f64 = (1.0 - (cos_b * cos_b)).sqrt();
let sin_c: f64 = (1.0 - (cos_c * cos_c)).sqrt();
// If two of the vertices are coincident, area is zero.
// Return early to avoid a divide by zero below.
if cos_a == 1.0 || cos_b == 1.0 || cos_c == 1.0 {
return 0.0;
}
// Calculate the cosine of the angles at the vertices
let cos_va = ((cos_a - (cos_b * cos_c)) / (sin_b * sin_c))
.max(-1.0)
.min(1.0);
let cos_vb = ((cos_b - (cos_c * cos_a)) / (sin_c * sin_a))
.max(-1.0)
.min(1.0);
let cos_vc = ((cos_c - (cos_a * cos_b)) / (sin_a * sin_b))
.max(-1.0)
.min(1.0);
// Calculate the angles themselves, in radians
let ang_va = cos_va.acos();
let ang_vb = cos_vb.acos();
let ang_vc = cos_vc.acos();
// Calculate and return the solid angle of the triangle
(ang_va + ang_vb + ang_vc - PI_64) as f32
}
/// Generates a uniform sample on a spherical triangle given two uniform
/// random variables i and j in [0, 1].
pub fn uniform_sample_spherical_triangle(
va: Vector,
vb: Vector,
vc: Vector,
i: f32,
j: f32,
) -> Vector {
// Calculate sines and cosines of the spherical triangle's edge lengths
let cos_a: f64 = dot(vb, vc).max(-1.0).min(1.0) as f64;
let cos_b: f64 = dot(vc, va).max(-1.0).min(1.0) as f64;
let cos_c: f64 = dot(va, vb).max(-1.0).min(1.0) as f64;
let sin_a: f64 = (1.0 - (cos_a * cos_a)).sqrt();
let sin_b: f64 = (1.0 - (cos_b * cos_b)).sqrt();
let sin_c: f64 = (1.0 - (cos_c * cos_c)).sqrt();
// If two of the vertices are coincident, area is zero.
// Return early to avoid a divide by zero below.
if cos_a == 1.0 || cos_b == 1.0 || cos_c == 1.0 {
// TODO: do something more intelligent here, in the case that it's
// an infinitely thin line.
return va;
}
// Calculate the cosine of the angles at the vertices
let cos_va = ((cos_a - (cos_b * cos_c)) / (sin_b * sin_c))
.max(-1.0)
.min(1.0);
let cos_vb = ((cos_b - (cos_c * cos_a)) / (sin_c * sin_a))
.max(-1.0)
.min(1.0);
let cos_vc = ((cos_c - (cos_a * cos_b)) / (sin_a * sin_b))
.max(-1.0)
.min(1.0);
// Calculate sine for A
let sin_va = (1.0 - (cos_va * cos_va)).sqrt();
// Calculate the angles themselves, in radians
let ang_va = cos_va.acos();
let ang_vb = cos_vb.acos();
let ang_vc = cos_vc.acos();
// Calculate the area of the spherical triangle
let area = ang_va + ang_vb + ang_vc - PI_64;
// The rest of this is from the paper "Stratified Sampling of Spherical
// Triangles" by James Arvo.
let area_2 = area * i as f64;
let s = (area_2 - ang_va).sin();
let t = (area_2 - ang_va).cos();
let u = t - cos_va;
let v = s + (sin_va * cos_c);
let q_top = (((v * t) - (u * s)) * cos_va) - v;
let q_bottom = ((v * s) + (u * t)) * sin_va;
let q = q_top / q_bottom;
let vc_2 =
(va * q as f32) + ((vc - (va * dot(vc, va))).normalized() * (1.0 - (q * q)).sqrt() as f32);
let z = 1.0 - (j * (1.0 - dot(vc_2, vb)));
(vb * z) + ((vc_2 - (vb * dot(vc_2, vb))).normalized() * (1.0 - (z * z)).sqrt())
}