require 'mittsu/math' module Mittsu class Ray attr_accessor :origin, :direction def initialize(origin = Mittsu::Vector3.new, direction = Mittsu::Vector3.new) @origin, @direction = origin, direction end def set(origin, direction) @origin.copy(origin) @direction.copy(direction) self end def copy(ray) @origin.copy(ray.origin) @direction.copy(ray.direction) self end def at(t, target = Mittsu::Vector3.new) target.copy(@direction).multiply_scalar(t).add(@origin) end def recast(t) v1 = Mittsu::Vector3.new @origin.copy(self.at(t, v1)) self end def closest_point_to_point(point, target = Mittsu::Vector3.new) target.sub_vectors(point, @origin) direction_distance = target.dot(@direction) if direction_distance < 0 return target.copy(@origin) end target.copy(@direction).multiply_scalar(direction_distance).add(@origin) end def distance_to_point(point) v1 = Mittsu::Vector3.new direction_distance = v1.sub_vectors(point, @origin).dot(@direction) # point behind the ray if direction_distance < 0 return @origin.distance_to(point) end v1.copy(@direction).multiply_scalar(direction_distance).add(@origin) v1.distance_to(point) end def distance_sq_to_segment(v0, v1, point_on_ray = nil, point_on_segment = nil) seg_center = Mittsu::Vector3.new seg_dir = Mittsu::Vector3.new diff = Mittsu::Vector3.new # from http:#www.geometrictools.com/LibMathematics/Distance/Wm5DistRay3Segment3.cpp # It returns the min distance between the ray and the segment # defined by v0 and v1 # It can also set two optional targets : # - The closest point on the ray # - The closest point on the segment seg_center.copy(v0).add(v1).multiply_scalar(0.5) seg_dir.copy(v1).sub(v0).normalize diff.copy(@origin).sub(seg_center) seg_extent = v0.distance_to(v1) * 0.5 a01 = -@direction.dot(seg_dir) b0 = diff.dot(@direction) b1 = -diff.dot(seg_dir) c = diff.length_sq det = (1.0 - a01 * a01).abs if det > 0 # The ray and segment are not parallel. s0 = a01 * b1 - b0 s1 = a01 * b0 - b1 ext_det = seg_extent * det if s0 >= 0 if s1 >= -ext_det if s1 <= ext_det # region 0 # Minimum at interior points of ray and segment. inv_det = 1.0 / det s0 *= inv_det s1 *= inv_det sqr_dist = s0 * (s0 + a01 * s1 + 2.0 * b0) + s1 * (a01 * s0 + s1 + 2.0 * b1) + c else # region 1 s1 = seg_extent s0 = [0.0, -(a01 * s1 + b0)].max sqr_dist = - s0 * s0 + s1 * (s1 + 2.0 * b1) + c end else # region 5 s1 = - seg_extent s0 = [0.0, -(a01 * s1 + b0)].max sqr_dist = -s0 * s0 + s1 * (s1 + 2.0 * b1) + c end else if s1 <= - ext_det # region 4 s0 = [0.0, -(-a01 * seg_extent + b0)].max s1 = (s0 > 0) ? -seg_extent : [[-seg_extent, -b1].max, seg_extent].min sqr_dist = - s0 * s0 + s1 * (s1 + 2 * b1) + c elsif s1 <= ext_det # region 3 s0 = 0.0 s1 = [[-seg_extent, -b1].max, seg_extent].min sqr_dist = s1 * (s1 + 2.0 * b1) + c else # region 2 s0 = [0.0, -(a01 * seg_extent + b0)].max s1 = (s0 > 0) ? seg_extent : [[-seg_extent, -b1].max, seg_extent].min sqr_dist = -s0 * s0 + s1 * (s1 + 2.0 * b1) + c end end else # Ray and segment are parallel. s1 = (a01 > 0) ? -seg_extent : seg_extent s0 = [0.0, -(a01 * s1 + b0)].max sqr_dist = -s0 * s0 + s1 * (s1 + 2.0 * b1) + c end if point_on_ray point_on_ray.copy(@direction).multiply_scalar(s0).add(@origin) end if point_on_segment point_on_segment.copy(seg_dir).multiply_scalar(s1).add(seg_center) end sqr_dist end def intersection_sphere?(sphere) self.distance_to_point(sphere.center) <= sphere.radius end def intersect_sphere(sphere, target = Mittsu::Vector3.new) # from http:#www.scratchapixel.com/lessons/3d-basic-lessons/lesson-7-intersecting-simple-shapes/ray-sphere-intersection/ v1 = Mittsu::Vector3.new v1.sub_vectors(sphere.center, @origin) tca = v1.dot(@direction) d2 = v1.dot(v1) - tca * tca radius2 = sphere.radius * sphere.radius return nil if d2 > radius2 thc = Math.sqrt(radius2 - d2) # t0 = first intersect point - entrance on front of sphere t0 = tca - thc # t1 = second intersect point - exit point on back of sphere t1 = tca + thc # test to see if both t0 and t1 are behind the ray - if so, return nil return nil if t0 < 0 && t1 < 0 # test to see if t0 is behind the ray: # if it is, the ray is inside the sphere, so return the second exit point scaled by t1, # in order to always return an intersect point that is in front of the ray. return self.at(t1, target) if t0 < 0 # else t0 is in front of the ray, so return the first collision point scaled by t0 self.at(t0, target) end def intersection_plane?(plane) # check if the ray lies on the plane first dist_to_point = plane.distance_to_point(@origin) return true if dist_to_point.zero? denominator = plane.normal.dot(@direction) return true if denominator * dist_to_point < 0 # ray origin is behind the plane (and is pointing behind it) false end def distance_to_plane(plane) denominator = plane.normal.dot(@direction) if denominator.zero? # line is coplanar, return origin return 0.0 if plane.distance_to_point(@origin).zero? # Null is preferable to nil since nil means.... it is nil return nil end t = -(@origin.dot(plane.normal) + plane.constant) / denominator # Return if the ray never intersects the plane t >= 0 ? t : nil end def intersect_plane(plane, target = Mittsu::Vector3.new) t = self.distance_to_plane(plane) return nil if t.nil? self.at(t, target) end def intersection_box?(box) v = Mittsu::Vector3.new !self.intersect_box(box, v).nil? end def intersect_box(box, target = Mittsu::Vector3.new) # http:#www.scratchapixel.com/lessons/3d-basic-lessons/lesson-7-intersecting-simple-shapes/ray-box-intersection/ invdirx = 1.0 / @direction.x invdiry = 1.0 / @direction.y invdirz = 1.0 / @direction.z origin = @origin if invdirx >= 0 tmin = (box.min.x - origin.x) * invdirx tmax = (box.max.x - origin.x) * invdirx else tmin = (box.max.x - origin.x) * invdirx tmax = (box.min.x - origin.x) * invdirx end if invdiry >= 0 tymin = (box.min.y - origin.y) * invdiry tymax = (box.max.y - origin.y) * invdiry else tymin = (box.max.y - origin.y) * invdiry tymax = (box.min.y - origin.y) * invdiry end return nil if tmin > tymax || tymin > tmax # These lines also handle the case where tmin or tmax is NaN # (result of 0 * Infinity). x != x returns true if x is NaN tmin = tymin if tymin > tmin || tmin != tmin tmax = tymax if tymax < tmax || tmax != tmax if invdirz >= 0 tzmin = (box.min.z - origin.z) * invdirz tzmax = (box.max.z - origin.z) * invdirz else tzmin = (box.max.z - origin.z) * invdirz tzmax = (box.min.z - origin.z) * invdirz end return nil if tmin > tzmax || tzmin > tmax tmin = tzmin if tzmin > tmin || tmin != tmin tmax = tzmax if tzmax < tmax || tmax != tmax #return point closest to the ray (positive side) return nil if tmax < 0 self.at(tmin >= 0 ? tmin : tmax, target) end def intersect_triangle(a, b, c, backface_culling, target = Mittsu::Vector3.new) # Compute the offset origin, edges, and normal. diff = Mittsu::Vector3.new edge1 = Mittsu::Vector3.new edge2 = Mittsu::Vector3.new normal = Mittsu::Vector3.new # from http:#www.geometrictools.com/LibMathematics/Intersection/Wm5IntrRay3Triangle3.cpp edge1.sub_vectors(b, a) edge2.sub_vectors(c, a) normal.cross_vectors(edge1, edge2) # Solve Q + t*D = b1*E1 + b2*E2 (Q = kDiff, D = ray direction, # E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by # |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2)) # |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q)) # |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N) d_dot_n = @direction.dot(normal) if d_dot_n > 0 return nil if backface_culling sign = 1.0 elsif d_dot_n < 0 sign = -1.0 d_dot_n = - d_dot_n else return nil end diff.sub_vectors(@origin, a) d_dot_q_x_e2 = sign * @direction.dot(edge2.cross_vectors(diff, edge2)) # b1 < 0, no intersection return nil if d_dot_q_x_e2 < 0 d_dot_e1_x_q = sign * @direction.dot(edge1.cross(diff)) # b2 < 0, no intersection return nil if d_dot_e1_x_q < 0 # b1+b2 > 1, no intersection return nil if d_dot_q_x_e2 + d_dot_e1_x_q > d_dot_n # Line intersects triangle, check if ray does. q_dot_n = -sign * diff.dot(normal) # t < 0, no intersection return nil if q_dot_n < 0 # Ray intersects triangle. self.at(q_dot_n / d_dot_n, target) end def apply_matrix4(matrix4) @direction.add(@origin).apply_matrix4(matrix4) @origin.apply_matrix4(matrix4) @direction.sub(@origin) @direction.normalize self end def ==(ray) ray.origin == @origin && ray.direction == @direction end def clone Mittsu::Ray.new.copy(self) end end end