package edu.stanford.rsl.conrad.rendering; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import edu.stanford.rsl.conrad.geometry.AbstractCurve; import edu.stanford.rsl.conrad.geometry.AbstractShape; import edu.stanford.rsl.conrad.geometry.shapes.compound.CompoundShape; import edu.stanford.rsl.conrad.geometry.shapes.compound.TriangleMesh; import edu.stanford.rsl.conrad.geometry.shapes.simple.PointND; import edu.stanford.rsl.conrad.geometry.shapes.simple.ProjectPointToLineComparator; import edu.stanford.rsl.conrad.geometry.shapes.simple.StraightLine; import edu.stanford.rsl.conrad.geometry.shapes.simple.Triangle; import edu.stanford.rsl.conrad.numerics.SimpleOperators; import edu.stanford.rsl.conrad.numerics.SimpleVector; import edu.stanford.rsl.conrad.physics.PhysicalObject; import edu.stanford.rsl.conrad.physics.PhysicalPoint; import edu.stanford.rsl.conrad.utils.CONRAD; /** * Abstract Class to model a ray caster. The ray caster casts rays through the scene and determines all his along a ray. * Then the ray caster determines the line segments between the objects and determines their representation. * * @author akmaier * */ public abstract class AbstractRayTracer { protected AbstractScene scene; protected ProjectPointToLineComparator comparator =null; /** * Inconsistent data correction will slow down the projection process, but will help to correct for problems in the data like an uneven number of hits of an object. */ protected boolean inconsistentDataCorrection = true; // Cache the information whether an object is a triangle or not private HashMap<PhysicalObject, Boolean> objIsTriangleCache = new HashMap<>(); /** * @return the scene */ public AbstractScene getScene() { return scene; } /** * @param scene the scene to set */ public void setScene(AbstractScene scene) { this.scene = scene; } /** * Method to cast a ray through the scene. Returns the edge segments which pass through different materials. * <BR><BR> * Rays must be normalized! * <BR> * @param ray * @return the list of line segments which were hit by the ray in the correct order */ public ArrayList<PhysicalObject> castRay(AbstractCurve ray) { ArrayList<PhysicalPoint> rayList = intersectWithScene(ray); if (rayList == null || rayList.size() == 0) { return null; } boolean doubles = false; // Filter double hits while (doubles){ boolean foundMore = false; int size = rayList.size(); for (int i = 0; i < size; i++){ for (int j = i+1; j < size; j++){ if (rayList.get(i).equals(rayList.get(j))){ foundMore = true; rayList.remove(j); size--; j--; } } } if (!foundMore) doubles = false; } if (rayList.size() > 0){ // sort the points along the ray direction in ascending or descending order ProjectPointToLineComparator clone = comparator.clone(); clone.setProjectionLine((StraightLine) ray); Collections.sort(rayList, clone); // filter consecutive points entering or leaving the object rayList = filterDoubles(rayList); if (rayList.size() == 0) { return null; } PhysicalPoint [] points = new PhysicalPoint[rayList.size()]; points = rayList.toArray(points); if (inconsistentDataCorrection){ // Count hits per object ArrayList<PhysicalObject> objects = new ArrayList<PhysicalObject>(); ArrayList<Integer> count = new ArrayList<Integer>(); for (int i = 0; i < points.length; i++){ PhysicalPoint p = points[i]; boolean found = false; for (int j =0; j< objects.size(); j++){ if (objects.get(j).equals(p.getObject())){ found = true; count.set(j, count.get(j) + 1); } } if (!found){ objects.add(p.getObject()); count.add(new Integer(1)); } } boolean rewrite = false; for (int i = 0; i < objects.size(); i++){ if (count.get(i) % 2 == 1){ boolean resolved = false; // Iterate over hits to find all hits on object i for (int j = 0; j < points.length; j++){ if (!resolved) { if (points[j].getObject().equals(objects.get(i))){ // only one hit of this object. no problem if (count.get(i) == 1){ points[j] = null; rewrite = true; resolved = true; } // three hits in a row of the same object. no problem. if (j > 0 && j < points.length-1) { // If we have three hits in a row on an object, remove the center hit (j) if (points[j-1].getObject().equals(objects.get(i)) && (points[j+1].getObject().equals(objects.get(i)))){ points[j] = null; rewrite = true; resolved = true; } } } } } if (!resolved){ // Still not resolved // remove center hit int toRemove = (count.get(i) + 1) / 2; int current = 0; for (int j = 0; j < points.length; j++){ if (points[j].getObject().equals(objects.get(i))){ current ++; if (current == toRemove){ points[j] = null; rewrite = true; resolved = true; } } } } } rayList = new ArrayList<PhysicalPoint>(); if (rewrite){ for (int j = 0; j < points.length; j++){ if (points[j] != null){ rayList.add(points[j]); } } points = new PhysicalPoint[rayList.size()]; points = rayList.toArray(points); } } } if (points.length == 0) { return null; } return computeMaterialIntersectionSegments(points); } else { return null; } } /** * Method to resolve the priority of the elements of the scene. * @param rayList * @return the correct line segments ordered according to the specified ray tracing order */ protected abstract ArrayList<PhysicalObject> computeMaterialIntersectionSegments(PhysicalPoint[] rayList); /** * Computes all intersection of the ray with the scene. * @param ray the ray through the scene * @return the intersection points. */ protected ArrayList<PhysicalPoint> intersectWithScene(AbstractCurve ray){ ArrayList<PhysicalPoint> rayList = new ArrayList<PhysicalPoint>(); SimpleVector smallIncrementAlongRay = SimpleOperators.subtract(ray.evaluate(CONRAD.SMALL_VALUE).getAbstractVector(), ray.evaluate(0).getAbstractVector()); // compute ray intersections: for (PhysicalObject shape: scene) { if (shape.getShape().getHitsOnBoundingBox(ray).size() > 0) { ArrayList<PointND> intersection = shape.intersectWithHitOrientation(ray); if (intersection != null && intersection.size() > 0){ for (PointND p : intersection){ PhysicalPoint point; // Clean coordinates of intersecting points with triangles, as the last coordinate is abused to return the inclination of the triangle if (objIsTriangle(shape)) { point = cleanTriangleIntersection(p); } else { point = new PhysicalPoint(p); } point.setObject(shape); rayList.add(point); } if(intersection.size() == 1) { PointND p = intersection.get(0); PhysicalPoint point; // When creating an opposing point, use the negative inclination if (objIsTriangle(shape)) { PhysicalPoint clean = cleanTriangleIntersection(p); p = new PointND(clean.getAbstractVector()); point = new PhysicalPoint(SimpleOperators.add(p.getAbstractVector(), smallIncrementAlongRay)); point.setHitOrientation(-clean.getHitOrientation()); } else { point = new PhysicalPoint(SimpleOperators.add(p.getAbstractVector(), smallIncrementAlongRay)); } point.setObject(shape); rayList.add(point); } else if(intersection.size() == 3) { PointND p = intersection.get(1); PhysicalPoint point; // Same as above if (objIsTriangle(shape)) { PhysicalPoint clean = cleanTriangleIntersection(p); p = new PointND(clean.getAbstractVector()); point = new PhysicalPoint(SimpleOperators.add(p.getAbstractVector(), smallIncrementAlongRay)); point.setHitOrientation(-clean.getHitOrientation()); } else { point = new PhysicalPoint(SimpleOperators.add(p.getAbstractVector(), smallIncrementAlongRay)); } point.setObject(shape); rayList.add(point); } } } } return rayList; } /** * When we hit a triangle, we store the inner product between the direction of the ray and the normal of the triangle to determine whether we just entered or left an object * Remove the point's abused coordinate and store this information in the physical point object * @param triangleHit point of intersection between ray and triangle. The point's last coordinate isn't a real coordinate * @return physical point with clean coordinates, the hit orientation and the object */ private PhysicalPoint cleanTriangleIntersection(PointND triangleHit) { double[] coordinates = triangleHit.getCoordinates(); double[] cleanCoordinates = Arrays.copyOf(coordinates, coordinates.length - 1); PhysicalPoint point = new PhysicalPoint(new PointND(cleanCoordinates)); point.setHitOrientation(coordinates[coordinates.length - 1]); return point; } /** * Determines whether the given object is a triangle or is composed out of (and only out of) triangles * The result is stored in a cache-like hash map such that the result can be quickly retrieved from the map * @param obj to check * @return true if the given object is a triangle, a triangle mesh or a compound shape which does not hold other objects than triangles */ private boolean objIsTriangle(PhysicalObject obj) { // First, try to retrieve the cached information if (objIsTriangleCache.containsKey(obj)) { return objIsTriangleCache.get(obj); } AbstractShape shape = obj.getShape(); if (shape instanceof Triangle || shape instanceof TriangleMesh) { objIsTriangleCache.put(obj, true); return true; } // do breadth first search on nested compound shapes else if (shape instanceof CompoundShape) { LinkedList<AbstractShape> queue = new LinkedList<>(); queue.add(shape); while(queue.size() > 0) { CompoundShape cs = (CompoundShape) queue.poll(); for (int i=0; i<cs.getInternalDimension(); i++) { shape = cs.get(i); if (shape instanceof Triangle || shape instanceof TriangleMesh) { continue; } else if (shape instanceof CompoundShape) { queue.add(shape); } else { objIsTriangleCache.put(obj, false); return false; } } } // if all the nested compound shapes do not hold more than triangles in the end, we're certainly dealing with a triangle objIsTriangleCache.put(obj, true); return true; } // otherwise it is another shape and thus not a triangle objIsTriangleCache.put(obj, false); return false; } /** * A ray is supposed to enter and leave a closed object composed out of triangles in an alternating order. If the ray e.g. enters the object twice, it's likely that the two intersections considered as entry are very close. * An intersecting point is considered an entry if the dot product of the ray's direction and the hit triangle's normal vector is smaller than 0 * @param rayList list of points where the ray intersects with an object * @return filtered list of points */ protected ArrayList<PhysicalPoint> filterDoubles(ArrayList<PhysicalPoint> rayList) { ArrayList<PhysicalPoint> filtered = new ArrayList<>(); Map<PhysicalObject, Boolean> nextEntersObject = new HashMap<>(); // object-specific switch whether the next intersection with a particular object enters or leaves the object // First intersection enters the object boolean normalsPointOutside = false; // yet to determine (but compiler forces to initialize this variable here) boolean init = false; // true if normalsPointOutside was truly initialized for (int i=0; i<rayList.size(); i++) { PhysicalPoint p = rayList.get(i); // Only for triangles if (!objIsTriangle(p.getObject())) { filtered.add(p); continue; } if (!init) { // Usually, the normal is defined to point out of the 3-D polygon. // However, some applications seem to export their polygons with normals pointing in the opposite way. // To be sure, we go by the direction of the first intersection. normalsPointOutside = (p.getHitOrientation() < 0); init = true; } // If this is the first intersection (by this ray) with the p's polygon, then add the polygon to the map if (!nextEntersObject.containsKey(p.getObject())) { nextEntersObject.put(p.getObject(), normalsPointOutside); } if (nextEntersObject.get(p.getObject()) && p.getHitOrientation() < 0) { // Ray enters the object filtered.add(p); nextEntersObject.put(p.getObject(), false); // toggle } else if (!nextEntersObject.get(p.getObject()) && p.getHitOrientation() > 0) { // Ray leaves the object filtered.add(p); nextEntersObject.put(p.getObject(), true); // toggle } else if (p.getHitOrientation() == 0) { // Ray is parallel to the triangle's surface // Or the hit orientation attribute was not set by the intersection test filtered.add(p); } // if we come to here, the point is discarded } return filtered; } } /* * Copyright (C) 2010-2014 Andreas Maier * CONRAD is developed as an Open Source project under the GNU General Public License (GPL). */