psychopath/src/fp_utils.rs
Nathan Vegdahl b315be0913 Fixed self-intersection bug for coordinates near zero.
This turned out to be a rather interesting one.  The water-tight
ray/triangle intersection algorithm, while very accurate for
finding if there is an intersection with a line segment, is
not as remarkably accurate for determining if that intersection
is within the interval of the ray.

This is because of the coordinate transformation it does
depending on ray direction: for triangles laying flat on one of
the axis planes near zero, that near-zero coordinate can get
transformed to a much less accurate space for testing.  In fact,
generally speaking, beause of the coordinate transform, you can
only rely on the test being as accurate as the least accurate
axis.

The ray-origin offset code was doing offsets based on the
assumption that the error on the major axes are independent, but
as this triangle intersection algorithm shows, you can't actually
depend on that being the case.  So rather than handling triangle
intersection as a special case, I've changed the intersection
position error to be a single float, representing the maximum
possible error on any axis.  This should be robust for any
geometry type added in the future, and also solves the immediate
issue in a correct way.
2017-07-10 00:52:28 -07:00

149 lines
3.4 KiB
Rust

//! Utilities for handling floating point precision issues
//!
//! This is based on the work in section 3.9 of "Physically Based Rendering:
//! From Theory to Implementation" 3rd edition by Pharr et al.
use math::{Point, Vector, Normal, dot};
#[inline(always)]
pub fn fp_gamma(n: u32) -> f32 {
use std::f32::EPSILON;
let e = EPSILON * 0.5;
(e * n as f32) / (1.0 - (e * n as f32))
}
pub fn increment_ulp(v: f32) -> f32 {
// Handle special cases
if (v.is_infinite() && v > 0.0) || v.is_nan() {
return v;
}
// Handle zero
let v = if v == -0.0 { 0.0 } else { v };
// Increase ulp by 1
if v >= 0.0 {
bits_to_f32(f32_to_bits(v) + 1)
} else {
bits_to_f32(f32_to_bits(v) - 1)
}
}
pub fn decrement_ulp(v: f32) -> f32 {
// Handle special cases
if (v.is_infinite() && v < 0.0) || v.is_nan() {
return v;
}
// Handle zero
let v = if v == 0.0 { -0.0 } else { v };
// Decrease ulp by 1
if v <= -0.0 {
bits_to_f32(f32_to_bits(v) + 1)
} else {
bits_to_f32(f32_to_bits(v) - 1)
}
}
pub fn robust_ray_origin(pos: Point, pos_err: f32, nor: Normal, ray_dir: Vector) -> Point {
// Get surface normal pointing in the same
// direction as ray_dir.
let nor = {
let nor = nor.into_vector();
if dot(nor, ray_dir) >= 0.0 { nor } else { -nor }
};
// Calculate offset point
let d = dot(nor.abs(), Vector::new(pos_err, pos_err, pos_err));
let offset = nor * d;
let p = pos + offset;
// Calculate ulp offsets
let x = if nor.x() >= 0.0 {
increment_ulp(p.x())
} else {
decrement_ulp(p.x())
};
let y = if nor.y() >= 0.0 {
increment_ulp(p.y())
} else {
decrement_ulp(p.y())
};
let z = if nor.z() >= 0.0 {
increment_ulp(p.z())
} else {
decrement_ulp(p.z())
};
Point::new(x, y, z)
}
#[inline(always)]
fn f32_to_bits(v: f32) -> u32 {
use std::mem::transmute_copy;
unsafe { transmute_copy::<f32, u32>(&v) }
}
#[inline(always)]
fn bits_to_f32(bits: u32) -> f32 {
use std::mem::transmute_copy;
unsafe { transmute_copy::<u32, f32>(&bits) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inc_ulp() {
assert!(increment_ulp(1.0) > 1.0);
assert!(increment_ulp(-1.0) > -1.0);
}
#[test]
fn dec_ulp() {
assert!(decrement_ulp(1.0) < 1.0);
assert!(decrement_ulp(-1.0) < -1.0);
}
#[test]
fn inc_ulp_zero() {
assert!(increment_ulp(0.0) > 0.0);
assert!(increment_ulp(0.0) > -0.0);
assert!(increment_ulp(-0.0) > 0.0);
assert!(increment_ulp(-0.0) > -0.0);
}
#[test]
fn dec_ulp_zero() {
assert!(decrement_ulp(0.0) < 0.0);
assert!(decrement_ulp(0.0) < -0.0);
assert!(decrement_ulp(-0.0) < 0.0);
assert!(decrement_ulp(-0.0) < -0.0);
}
#[test]
fn inc_dec_ulp() {
assert_eq!(decrement_ulp(increment_ulp(1.0)), 1.0);
assert_eq!(decrement_ulp(increment_ulp(-1.0)), -1.0);
assert_eq!(decrement_ulp(increment_ulp(1.2)), 1.2);
assert_eq!(decrement_ulp(increment_ulp(-1.2)), -1.2);
}
#[test]
fn dec_inc_ulp() {
assert_eq!(increment_ulp(decrement_ulp(1.0)), 1.0);
assert_eq!(increment_ulp(decrement_ulp(-1.0)), -1.0);
assert_eq!(increment_ulp(decrement_ulp(1.2)), 1.2);
assert_eq!(increment_ulp(decrement_ulp(-1.2)), -1.2);
}
}