Hi!
I'm very new to Java (I literally started learning it yesterday) and I've been working on a simple program that's meant to simulate interactions between optics objects and light rays.
Everything has been going really well, except that my mirrors only reflect the first ray that comes in contact with them.
Here's a screenshot:
Screen Shot 2014-09-13 at 7.39.23 PM.jpg
I wrote 7 classes:
- Main class to create the optics object
- Optics class. the most important class that uses a recursive function to detect the nearest intersection point of a ray with the nearest optics object. It then calculates a reflected ray and inputs it back into the function until no intersections are found or the max recursion depth is met.
- Ray. creates a rays (in parametric form O + tD where O is the origin, D is the direction and t is a non-negative scalar)
- Object. empty abstract class used for polymorphism (so when i add more optics objects like prisms and lenses they'll share the object type)
- Mirror. Basically a line segment created with the Line class
- Line. Creates a line segment
- Draw. JComponent with paintComponent function that loops through an array of shapes and draws them to the screen.
What I know so far is that the problem boils down to the checkRay() function in the Optics class.
When a mirror has already reflected a ray, the line:
creates an intersection with a null point.Intersection sec = new Intersection(ray,(Mirror)obj);
I debugged it line by line and found the problem was that my variable 't' (that is the parameter for the parametric line which represents the mirror) in the Intersection class got big values when it's meant to be between 0 and 1 (since it's a line segment), which resulted in the function returning null (as it should when 't' is not between 0 and 1).
I've confirmed with tests that this has nothing to do with the specific mirror or angle of incidence. It only occurs when a mirror has already been intersected with by a ray.
I can't figure out why this is happening but I hope it's enough information to work with.
**Edit:
I've found out that my function:
changes the value of the 'D' PVector of the mirror inside my 'objects' ArrayList.public PVector getNormal(PVector D){ D.normalize(); return new PVector(D.y,-D.x); }
How can it access the private PVector 'D' from outside the Mirror class?
This normalization to the direction vector is what causes the Intersection class to return null the second time around!
****Edit:
I solved it. The problem was that in the getNormal function, the input vector argument was a reference to the 'D' vector for the mirror in my 'objects' ArrayList so the .normalize() function acted upon the original vector, changing it's value and screwing things up.
Thanks for the help!
The two classes I talked about:
Optics: (note line 64. this line returns a null intersection when it shouldn't)
package ofer.davidson; import java.util.ArrayList; public class Optics { //Arrays to store all the optics objects and rays that are created ArrayList<Object> objects; ArrayList<Ray> rays; //previous intersection point Intersection prev = null; //maximum recursion depth final int MAX_DEPTH = 64; //current recursion depth private int depth = 0; public Optics() { //initialize the arraylists objects = new ArrayList<Object>(); rays = new ArrayList<Ray>(); } public void addObject(Object obj) { objects.add(obj); } public void addRay(Ray ray){ rays.add(ray); } public void display(){ //loop through rays list and for every one of them check the ray for intersections for(Ray ray : rays){ prev = null; checkRay(ray); } } //this is a recursive funtion that accepts a ray as an argument and checks whether //the ray intersects an object. if it does, it will create a new ray and modify //it according to the object it intersected with then check the newly created ray //for intersections and so on private void checkRay(Ray ray){ //this variable will be set to true if the ray intersects a mirror boolean intersected = false; //arraylist that will contain all the detected intersection objects ArrayList<Intersection> intersections = new ArrayList<Intersection>(); //aaraylist to contain the optics objects that correspond with the intersection objects ArrayList<Object> intersectedObj = new ArrayList<Object>(); //origin and direction of ray to be used in calculating the reflected ray and in the drawing of the lines PVector O = ray.getOrigin(); PVector D = ray.getDirection(); //loop through all objects for(Object obj : objects){ //check for intersection between ray and mirror Intersection sec = new Intersection(ray,(Mirror)obj); //if the intersection point exists if(sec.getPoint() != null){ //add the intersection and the corresponding object to appropriate arraylist intersections.add(sec); intersectedObj.add(obj); //declare that an intersectio had been detected intersected = true; } } //if an intersection has been detected and the recursion depth hasn't exceeded the maximum if(intersected && depth < MAX_DEPTH){ depth ++; //declare variable d that will be used to find the intersection closest to the origin of the ray float d = Float.MAX_VALUE; //objects to store the closest intersection and object Intersection close = null; Object closeObj = null; //loop through the collected intersectin objects for(int i = 0; i < intersections.size(); i++){ //get object from arraylist Intersection sec = intersections.get(i); //check if the distance between the intersection point and the origin of the line is smaller than any previously found distance (stored in d) if(dist(sec.x,sec.y,O.x,O.y)<d){ //if the distance is smaller, update d to this distance and set the two objects to the closest object and intersection discovered d = dist(sec.x,sec.y,O.x,O.y); close = sec; closeObj = intersectedObj.get(i); } } //update previous intersection to new intersection prev = close; //draw a line from ray origin to intersection point Draw.line(O.x,O.y,close.x,close.y); //reflect the ray direction vector along the mirrors normal vector Mirror mirror = (Mirror)closeObj; PVector R = getReflected(D,getNormal(mirror.getDirection())); //create the reflected ray Ray reflected = new Ray(close.getPoint(),R); //recurse, check the newly created ray for intersections checkRay(reflected); } //if no intersection has been detected else{ //and the original ray has intersected at least one object if(prev != null){ //draw a line from the previous intersection point, in the direction of the ray PVector R = ray.getDirection(); Draw.line(prev.x,prev.y,prev.x+R.x*100000,prev.y+R.y*100000); } //if the original ray has not intersected anything else{ //draw a line from the origin of the ray in the direction of the ray O = ray.getOrigin(); D = ray.getDirection(); Draw.line(O.x, O.y,O.x + D.x*100000,O.y+D.y*100000); } } } //keeps angle positive and in the 0-360 range public static float fixAngle(float e) { return (float) ((e + Math.PI * 2)% (Math.PI * 2)); } //returns the normal to the supplied vector public static PVector getNormal(PVector D){ D.normalize(); return new PVector(D.y,-D.x); } //returns the supplied vector L, reflected along the normal N public static PVector getReflected(PVector L, PVector N){ L = PVector.mult(L,-1); L.normalize(); float dot = PVector.dot(N,L); return new PVector(2*N.x*dot-L.x, 2*N.y*dot-L.y); } //returns the distance between two points public static float dist(float x1, float y1, float x2, float y2){ return (float) Math.sqrt(Math.pow((x1-x2),2) + Math.pow((y1 - y2),2)); } }
Intersection: (note line 27. this is where t gets it's value)
package ofer.davidson; public class Intersection { public float x; public float y; private PVector sec; // intersection point //intersetion of ray and line segment public Intersection(Ray ray, Mirror mirror){ PVector P0 = ray.getOrigin(); //position of the ray PVector P1 = mirror.getStart(); //origin of the mirror PVector D0 = ray.getDirection(); //direction of the ray PVector D1 = mirror.getDirection(); //direction of the mirror PVector delta = PVector.sub(P1,P0); //direction from mirror to ray PVector D1perp = new PVector(-D1.y,D1.x); //normal to mirror direction PVector D0perp = new PVector(-D0.y,D0.x); //normal to ray direction float D0dotD1perp = D0.dot(D1perp); //dot perp operation //if dot perp is 0 it means the lines are parallel or the same line meaning they dont intersect if(D0dotD1perp != 0){ //fiding the parameters using the parametric equation for a mirror: P+t*D float s = delta.dot(D1perp)/D0dotD1perp; float t = delta.dot(D0perp)/D0dotD1perp; //for a ray the parameter has to be greater than 0 and for a line segment it must be between 0 and 1 if(s > 0 && t >= 0 && t<= 1){ sec = PVector.add(P0,PVector.mult(D0,s)); x = sec.x; y = sec.y; } else this.returnNull(); } else this.returnNull(); } private Intersection returnNull(){ return null; } public PVector getPoint(){ return sec; } }
The rest of 'em:
Main:
package ofer.davidson; import java.awt.*; import javax.swing.*; @SuppressWarnings("serial") public class Main extends JFrame{ public static void main(String[] args) { new Main(); } public Main(){ //setup this.setSize(500,500); this.setTitle("Optics Simulator"); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.add(new Draw() ,BorderLayout.CENTER); this.setVisible(true); //create objects Optics optics = new Optics(); Ray ray = new Ray(0,0,1,1); Mirror mirror = new Mirror(new PVector(300,300),(float)Math.toRadians(90),200); Mirror mirror2 = new Mirror(new PVector(100,400),(float)Math.toRadians(50),200); //add to optics so they will react optics.addRay(ray); optics.addObject(mirror); optics.addObject(mirror2); //display the objects mirror.display(); mirror2.display(); optics.display(); repaint(); } }
Ray:
package ofer.davidson; public class Ray{ private PVector O; //origin private PVector D; //direction private float a; //angle //ray from origin and direction public Ray(PVector O, PVector D){ this.O = O; this.D = D; this.a = Optics.fixAngle((float)(Math.atan(D.y/D.x))); } //ray from point and direction components public Ray(float x, float y, float a, float b){ this.O = new PVector(x,y); this.D = new PVector(a,b); this.a = Optics.fixAngle((float)Math.atan(b/a)); } //ray from point and angle public Ray(float x, float y, float a){ this.O = new PVector(x,y); this.a = a; this.D = new PVector((float)Math.cos(a),(float)Math.sin(a)); } //returns origin of ray public PVector getOrigin(){ return O; } //returns direction of ray public PVector getDirection(){ return D; } //returns angle of ray public float getAngle(){ return a; } }
Object:
package ofer.davidson; abstract class Object { //superclass for all optics objects (used for polymorphism) }
Mirror:
package ofer.davidson; public class Mirror extends Object{ private Line mirror; //line primative private PVector S; //start private PVector E; //end private PVector D; //Direction //mirror from two points public Mirror(float x1, float y1, float x2, float y2){ //its basically a line mirror = new Line(x1,y1,x2,y2); updateVars(); } //mirror from origin, angle, and length public Mirror(PVector O,float a, float l){ mirror = new Line(O,a,l); updateVars(); } private void updateVars(){ S = mirror.getStart(); E = mirror.getEnd(); D = mirror.getDirection(); } //draw a line to display the mirror public void display(){ Draw.line(S.x, S.y, E.x, E.y); } public PVector getStart(){ return S; } public PVector getDirection(){ return D; } public PVector getEnd(){ return E; } }
Line:
package ofer.davidson; public class Line{ //Line segement (not ray or line) private PVector S; //Position private PVector D; //Direction private PVector O; //Origin private PVector E; //End public float x1; public float y1; public float x2; public float y2; private float l; //Length private float a; //Rotation //should the line be drawn from the center outwards or from one point to another private String mode = "CENTER"; private String[] modes = {"CENTER","START"}; //Line from start and end points public Line(PVector S, PVector E){ this.S = S; this.E = E; //calculate the direction D = PVector.sub(E,S); updateXY(); } //line from start and end points using vector components public Line(float x1, float y1, float x2, float y2){ S = new PVector(x1,y1); E = new PVector(x2,y2); D = new PVector(x1-x2,y1-y2); D = PVector.sub(E,S); updateXY(); } //line from origin, angle and length public Line(PVector O,float a, float l){ if(mode == "CENTER"){ //creates the line from the center this.O = new PVector(O.x,O.y); this.a = a; this.l = l; S = new PVector(O.x-(float)Math.cos(a)*l/2, O.y-(float)Math.sin(a)*l/2); E = new PVector(O.x+(float)Math.cos(a)*l/2, O.y+(float)Math.sin(a)*l/2); } else if(mode == "START"){ //creates the line from the origin S = O; E = new PVector(S.x-(float)Math.cos(a)*l,S.y-(float)Math.sin(a)*l); } D = PVector.sub(E,S); updateXY(); } //change the line mode public void lineMode(String mode){ for(int i=0;i<modes.length;i++) if(modes[i] == mode) this.mode = mode; } private void updateXY(){ this.x1 = S.x; this.y1 = S.y; this.x2 = E.x; this.y2 = E.y; } public PVector getStart(){ return S; } public PVector getDirection(){ return D; } public PVector getEnd(){ return E; } public PVector getOrigin(){ return O; } public float getLength(){ return l; } public float getAngle(){ return a; } }
Draw:
package ofer.davidson; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.Line2D; import java.util.ArrayList; import javax.swing.JComponent; @SuppressWarnings("serial") public class Draw extends JComponent{ //an arraylist containing all the created shapes static ArrayList<Shape> shapes = new ArrayList<Shape>(); //method that will draw everything onscreen public void paintComponent(Graphics g){ Graphics2D canvas = (Graphics2D)g; this.setBackground(Color.WHITE); //use antialiasing canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); //stroke color is black canvas.setPaint(Color.BLACK); //loop through shapes arraylist and draw every shape on the canvas for(Shape s : shapes){ canvas.draw(s); } } //add a line to the shapes arraylist public static Line2D.Float line(float x1, float y1, float x2, float y2){ Line2D.Float line = new Line2D.Float(x1,y1,x2,y2); shapes.add(line); return line; } //clear the canvas public static void reset(){ shapes.clear(); } }
By the way, If I did everything in a really inefficient or dumb way I'd love to know!
Also why doesn't my background turn white??
Thanks so much for reading!
I'm really enjoying writing this thing