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.
149 lines
3.4 KiB
Rust
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);
|
|
}
|
|
|
|
}
|