def trace_path(self, ray: Ray, container_node: Node, intersection_node: Node, intersection_point: tuple) -> Tuple[Ray, Node]: """ Determines if the ray is absorbed or scattered along it's path and returns a new ray is these processes occur. If not, or the container node does not have a material attached the ray is moved to the next intersection point. """ # Exit early if possible if any([node.geometry is None for node in (container_node, intersection_node)]): raise TraceError("Node is missing a geometry.") elif container_node.geometry.material is None: logger.debug("Container node is missing a material. Will propagate ray the full path length.") new_ray = replace(ray, position=intersection_point) hit_node = intersection_node return (new_ray, hit_node) # Have a proper material root = self.scene.root distance = distance_between(ray.position, intersection_point) volume = make_volume(container_node, distance) new_ray = volume.trace(ray) position_in_intersection_node_frame = root.point_to_node(new_ray.position, intersection_node) if intersection_node.geometry.is_on_surface(position_in_intersection_node_frame): # Hit interface between container node and intersection node hit_node = intersection_node else: # Hit molecule in container node (i.e. was absorbed or scattered) hit_node = container_node return new_ray, hit_node
def step(ray, points, nodes, renderer=None): """ Step the ray through the scene until the next Monte Carlo decision. This is generator function because it cannot be known exactly how many events will occur on a give step and allows much of the state of the ray to be reused rather than recalculated. Parameters ---------- ray : Ray The ray being traced. It must *not* be on the surface of a node. points : tuple A tuple of point tuples like ((float, float, float), ...) nodes : tuple A tuple of Node objects. Raises ------ TraceError If logical error occurs. Yields ------ tuple A tuple of (Ray, Decision) objects. """ container, to_node, surface_node = ray_status(ray, points, nodes) min_point = ray.position max_point = points[0] dist = distance_between(min_point, max_point) _ray = ray for (ray, decision) in trace_path(ray, container, dist): if renderer: renderer.add_ray_path([_ray, ray]) _ray = ray yield ray, decision if to_node is None and container.parent is None: # Case: Hit world node; kill ray here. ray = replace(ray, is_alive=False) yield ray, Decision.KILL elif points_equal(ray.position, max_point): # Case: Hit surface # NB The ray argument of `trace_surface` *must* be a ray on the surface of the # node and the returned ray must *not* be on the node! before_ray = ray _ray = ray for ray, decision in trace_surface(ray, container, to_node, surface_node): if renderer: renderer.add_ray_path([_ray, ray]) _ray = ray yield ray, decision # Avoid error checks in production if __debug__: local_ray = ray.representation(surface_node.root, surface_node) if surface_node.geometry.is_on_surface(local_ray.position): logger.warning("(before) pos: {}".format(before_ray.position)) logger.warning("(after) pos: {}".format(ray.position)) raise TraceError("After tracing a surface the ray cannot still be on the surface.")
def next_hit(scene, ray): """ Returns information about the next interface the ray makes with the scene. Parameters ---------- scene : Scene ray : Ray Returns ------- hit_node : Node The node corresponding to the geometry object that was hit. interface : tuple of Node Two node: the `container` and the `adjacent` which correspond to the materials either side of the interface. point: tuple of float The intersection point. distance: float Distance to the intersection point. """ # Intersections are in the local node's coordinate system intersections = scene.intersections(ray.position, ray.direction) # Remove on surface intersections intersections = \ [x for x in intersections if not close_to_zero(x.distance)] # Convert intersection points to world node intersections = [x.to(scene.root) for x in intersections] # Node which owns the surface if len(intersections) == 0: return None # The surface being hit hit = intersections[0] if len(intersections) == 1: hit_node = hit.hit return hit_node, (hit_node, None), hit.point, hit.distance container = find_container(intersections) hit = intersections[0] # Intersection point and distance from ray point = hit.point hit_node = hit.hit distance = distance_between(ray.position, point) if container == hit_node: adjacent = intersections[1].hit else: adjacent = hit_node return hit_node, (container, adjacent), point, distance
def intersections(self, ray_origin, ray_direction) -> Sequence[Intersection]: """ Returns intersections with node's geometry and child subtree. Parameters ---------- ray_origin : tuple of float The ray position `(x, y, z)`. ray_direction : tuple of float The ray position `(a, b, c)`. Returns ------- all_intersections : tuple of Intersection All intersection with this scene and a list of Intersection objects. """ all_intersections = [] if self.geometry is not None: points = self.geometry.intersections(ray_origin, ray_direction) for point in points: intersection = Intersection( coordsys=self, point=point, hit=self, distance=distance_between(ray_origin, point), ) all_intersections.append(intersection) all_intersections = tuple(all_intersections) for child in self.children: # Intersections with node's geometry ray_origin_in_child = self.point_to_node(ray_origin, child) ray_direction_in_child = self.vector_to_node(ray_direction, child) # Intersections with node's subtree intersections_in_child = child.intersections( ray_origin_in_child, ray_direction_in_child ) all_intersections = all_intersections + intersections_in_child return all_intersections