use crate::Float; use crate::core::geometry::{ Bounds3f, DirectionCone, Normal3f, Point2f, Point3f, Point3fi, Ray, Vector2f, Vector3f, VectorLike, }; use crate::core::interaction::{Interaction, InteractionTrait, SurfaceInteraction}; use crate::core::shape::{ShapeIntersection, ShapeSample, ShapeSampleContext, ShapeTrait}; use crate::utils::gpu_array_from_fn; use crate::utils::math::{clamp, lerp, square}; use crate::utils::splines::{ bound_cubic_bezier, cubic_bezier_control_points, evaluate_cubic_bezier, subdivide_cubic_bezier, }; use crate::utils::transform::{Transform, look_at}; use num_traits::Float as NumFloat; #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq)] pub enum CurveType { Flat, Cylinder, Ribbon, } #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct CurveCommon { pub curve_type: CurveType, pub cp_obj: [Point3f; 4], pub width: [Float; 2], pub n: [Normal3f; 2], pub normal_angle: Float, pub inv_sin_normal_angle: Float, pub render_from_object: Transform, pub object_from_render: Transform, pub reverse_orientation: bool, pub transform_swap_handedness: bool, } impl CurveCommon { #[allow(clippy::too_many_arguments)] pub fn new( c: &[Point3f], w0: Float, w1: Float, curve_type: CurveType, norm: &[Normal3f], render_from_object: Transform, object_from_render: Transform, reverse_orientation: bool, ) -> Self { let transform_swap_handedness = render_from_object.swaps_handedness(); let width = [w0, w1]; assert_eq!(c.len(), 4); let cp_obj: [Point3f; 4] = c[..4].try_into().unwrap(); let mut n: [Normal3f; 2] = gpu_array_from_fn(|_| Normal3f::default()); let mut normal_angle: Float = 0.; let mut inv_sin_normal_angle: Float = 0.; if norm.len() == 2 { n[0] = norm[0].normalize(); n[1] = norm[1].normalize(); normal_angle = n[0].angle_between(n[1]); inv_sin_normal_angle = 1. / normal_angle.sin(); } Self { curve_type, cp_obj, width, n, normal_angle, inv_sin_normal_angle, render_from_object, object_from_render, reverse_orientation, transform_swap_handedness, } } } #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct CurveShape { pub common: CurveCommon, pub u_min: Float, pub u_max: Float, } #[repr(C)] #[derive(Clone, Copy, Debug)] struct IntersectionContext { pub ray: Ray, pub object_from_ray: Transform, pub common: CurveCommon, } impl CurveShape { pub fn new(common: CurveCommon, u_min: Float, u_max: Float) -> Self { Self { common, u_min, u_max, } } fn intersect_ray(&self, r: &Ray, t_max: Float) -> Option { let ray = self .common .object_from_render .apply_to_ray(r, &mut Some(t_max)); let cp_obj = cubic_bezier_control_points(&self.common.cp_obj, self.u_min, self.u_max); // Project curve control points to plane perpendicular to ray let mut dx = ray.d.cross(cp_obj[3] - cp_obj[0]); if dx.norm_squared() == 0. { (dx, _) = ray.d.coordinate_system(); } let ray_from_object = look_at(ray.o, ray.o + ray.d, dx).expect("Inversion error"); let cp: [Point3f; 4] = gpu_array_from_fn(|i| ray_from_object.apply_to_point(cp_obj[i])); let max_width = lerp(self.u_min, self.common.width[0], self.common.width[1]).max(lerp( self.u_max, self.common.width[0], self.common.width[1], )); let curve_bounds = Bounds3f::from_points(cp[0], cp[1]) .union(Bounds3f::from_points(cp[2], cp[3])) .expand(0.5 * max_width); let ray_bounds = Bounds3f::from_points(Point3f::zero(), Point3f::new(0., 0., ray.d.norm() * t_max)); if !ray_bounds.overlaps(&curve_bounds) { return None; } let l0 = (0..2) .map(|i| { (cp[i].x() - 2.0 * cp[i + 1].x() + cp[i + 2].x()) .abs() .max((cp[i].y() - 2.0 * cp[i + 1].y() + cp[i + 2].y()).abs()) .max((cp[i].z() - 2.0 * cp[i + 1].z() + cp[i + 2].z()).abs()) }) .fold(0.0, Float::max); let max_depth = if l0 > 0. { let eps = self.common.width[0].max(self.common.width[1]) * 0.05; let r0: i32 = (1.41421356237 * 6. * l0 / (8. * eps)).log2() as i32 / 2; clamp(r0, 0, 10) } else { 0 }; let context = IntersectionContext { ray, object_from_ray: ray_from_object.inverse(), common: self.common.clone(), }; self.recursive_intersect(&context, t_max, &cp, self.u_min, self.u_max, max_depth) } fn recursive_intersect( &self, context: &IntersectionContext, mut t_max: Float, cp: &[Point3f], u0: Float, u1: Float, depth: i32, ) -> Option { if depth > 0 { let cp_split = subdivide_cubic_bezier(cp); let u = [u0, (u0 + u1) / 2., u1]; let mut best_hit: Option = None; for seg in 0..2 { let cps: &[Point3f] = &cp_split[3 * seg..3 * seg + 4]; let max_width = lerp(u[seg], self.common.width[0], self.common.width[1]).max(lerp( u[seg + 1], self.common.width[0], self.common.width[1], )); let curve_bounds = Bounds3f::from_points(cps[0], cps[1]) .union(Bounds3f::from_points(cps[2], cps[3])) .expand(0.5 * max_width); let ray_bounds = Bounds3f::from_points( Point3f::zero(), Point3f::new(0., 0., context.ray.d.norm() * t_max), ); if !ray_bounds.overlaps(&curve_bounds) { continue; } if let Some(hit) = self.recursive_intersect(context, t_max, cps, u[seg], u[seg + 1], depth - 1) { best_hit = Some(hit); t_max = best_hit.as_ref().unwrap().t_hit; } } best_hit } else { self.intersect_segment(context, t_max, cp, u0, u1) } } fn intersect_segment( &self, context: &IntersectionContext, t_max: Float, cp: &[Point3f], u0: Float, u1: Float, ) -> Option { let edge1 = (cp[1].y() - cp[0].y()) * -cp[0].y() + cp[0].x() * (cp[0].x() - cp[1].x()); let edge2 = (cp[2].y() - cp[3].y()) * -cp[3].y() + cp[3].x() * (cp[3].x() - cp[2].x()); if edge1 <= 0.0 || edge2 <= 0.0 { return None; } let segment_dir = Point2f::new(cp[3].x(), cp[3].y()) - Point2f::new(cp[0].x(), cp[0].y()); let denom = segment_dir.norm_squared(); if denom == 0. { return None; } let w = Vector2f::new(cp[0].x(), cp[0].y()).dot(-segment_dir) / denom; let u = clamp(lerp(w, u0, u1), u0, u1); let ray_length = context.ray.d.norm(); let mut hit_width = lerp(u, self.common.width[0], self.common.width[1]); let mut n_hit = Normal3f::zero(); if let CurveType::Ribbon = context.common.curve_type { n_hit = if context.common.normal_angle == 0. { context.common.n[0] } else { let sin0 = ((1. - u) * self.common.normal_angle).sin() * self.common.inv_sin_normal_angle; let sin1 = (u * self.common.normal_angle).sin() * self.common.inv_sin_normal_angle; sin0 * self.common.n[0] + sin1 * self.common.n[1] }; hit_width = hit_width * n_hit.dot(context.ray.d.into()).abs() / ray_length; } let (pc, dpcdw) = evaluate_cubic_bezier(cp, clamp(w, 0., 1.)); let ray_length = context.ray.d.norm(); if !self.valid_hit(pc, hit_width, t_max, ray_length) { return None; } // Hit is valid, obtain normals, and interaction differentials self.intersection_result(context, pc, dpcdw, u, hit_width, n_hit, ray_length) } fn hit_ribbon( &self, u: Float, hit_width: Float, context: &IntersectionContext, ) -> (Float, Normal3f) { let n_hit = if context.common.normal_angle == 0. { context.common.n[0] } else { let sin0 = ((1. - u) * self.common.normal_angle).sin() * self.common.inv_sin_normal_angle; let sin1 = (u * self.common.normal_angle).sin() * self.common.inv_sin_normal_angle; sin0 * self.common.n[0] + sin1 * self.common.n[1] }; let new_hit_width = hit_width * n_hit.dot(context.ray.d.into()).abs() / context.ray.d.norm(); (new_hit_width, n_hit) } fn valid_hit(&self, pc: Point3f, hit_width: Float, t_max: Float, ray_length: Float) -> bool { let pt_curve_dist_sq = square(pc.x()) + square(pc.y()); if pt_curve_dist_sq > square(hit_width * 0.5) || pc.z() < 0.0 || pc.z() > ray_length * t_max { return false; } true } #[allow(clippy::too_many_arguments)] fn intersection_result( &self, context: &IntersectionContext, pc: Point3f, dpcdw: Vector3f, u: Float, hit_width: Float, n_hit: Normal3f, ray_length: Float, ) -> Option { let t_hit = pc.z() / ray_length; let pt_curve_dist = (square(pc.x()) + square(pc.y())).sqrt(); let edge_func = dpcdw.x() * -pc.y() + dpcdw.y() * pc.x(); let v = if edge_func > 0. { 0.5 + pt_curve_dist / hit_width } else { 0.5 - pt_curve_dist / hit_width }; let (_, dpdu) = evaluate_cubic_bezier(&self.common.cp_obj, u); let dpdv = match context.common.curve_type { CurveType::Ribbon => Vector3f::from(n_hit).cross(dpdu).normalize() * hit_width, _ => { let dpdu_plane = context.object_from_ray.apply_inverse_vector(dpdu); let mut dpdv_plane = Vector3f::new(-dpdu_plane.y(), dpdu_plane.x(), 0.).normalize() * hit_width; if context.common.curve_type == CurveType::Cylinder { let theta = lerp(v, -90., 90.); let rot = Transform::rotate_around_axis(-theta, dpdu_plane); dpdv_plane = rot.apply_to_vector(dpdv_plane); } context.object_from_ray.apply_to_vector(dpdv_plane) } }; let p_error = Vector3f::fill(hit_width); let flip_normal = self.common.reverse_orientation ^ self.common.transform_swap_handedness; let pi = Point3fi::new_with_error(context.ray.at(t_hit), p_error); let intr = SurfaceInteraction::new( pi, Point2f::new(u, v), -context.ray.d, dpdu, dpdv, Normal3f::default(), Normal3f::default(), context.ray.time, flip_normal, ); Some(ShapeIntersection { intr, t_hit }) } } impl ShapeTrait for CurveShape { fn bounds(&self) -> Bounds3f { let cs_span = self.common.cp_obj; let obj_bounds = bound_cubic_bezier(&cs_span, self.u_min, self.u_max); let width0 = lerp(self.u_min, self.common.width[0], self.common.width[1]); let width1 = lerp(self.u_max, self.common.width[0], self.common.width[1]); let obj_bounds_expand = obj_bounds.expand(width0.max(width1) * 0.5); self.common .render_from_object .apply_to_bounds(obj_bounds_expand) } fn normal_bounds(&self) -> DirectionCone { DirectionCone::entire_sphere() } fn area(&self) -> Float { let cp_obj = cubic_bezier_control_points(&self.common.cp_obj, self.u_min, self.u_max); let width0 = lerp(self.u_min, self.common.width[0], self.common.width[1]); let width1 = lerp(self.u_max, self.common.width[0], self.common.width[1]); let avg_width = (width0 + width1) / 2.; let mut approx_length = 0.; for i in 0..3 { approx_length += cp_obj[i].distance(cp_obj[i + 1]); } approx_length * avg_width } fn intersect_p(&self, ray: &Ray, t_max: Option) -> bool { self.intersect_ray(ray, t_max.unwrap_or(Float::INFINITY)) .is_some() } fn intersect(&self, ray: &Ray, t_max: Option) -> Option { self.intersect_ray(ray, t_max.unwrap_or(Float::INFINITY)) } fn pdf(&self, _interaction: &Interaction) -> Float { unimplemented!() } fn pdf_from_context(&self, _ctx: &ShapeSampleContext, _wi: Vector3f) -> Float { unimplemented!() } fn sample(&self, _u: Point2f) -> Option { unimplemented!() } fn sample_from_context(&self, _ctx: &ShapeSampleContext, _u: Point2f) -> Option { unimplemented!() } }