JOGL: 3D Sphere/Plane Collision (distance between 3D point and plane)

I’m in a 3D graphics class that uses JOGL for its rendering.

My current assignment is to get a sphere bouncing around the inside of the box. I’ve got that working. I’ve even got multiple spheres bouncing off of one another. Hooray.

But now I want to add some 3D shapes for the spheres to bounce off of. I understand how to do the bouncing once I’ve got the closest point on a plane to my sphere, but the question is: given a point in 3d space, and a polygon defined as 3 other points in 3d space, how do I find the point on that polygon closest to the first point? If I know that, then I can do the collision detection (compare the distance between that and the center of the sphere to the radius of the sphere) as well as bouncing in the correct direction (reflect the delta vector around that point).

I’ve put together a small test program that contains all of the essentials. I’ve left out the actual drawing of the sphere (since that has nothing to do with the problem), so right now it’s just a point:


import java.awt.BorderLayout;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.swing.JFrame;

import com.sun.opengl.util.Animator;
import com.sun.opengl.util.FPSAnimator;

public class SpherePlaneTest implements GLEventListener {

	private final GLCanvas canvas = new GLCanvas();

	private final int worldX = -400;
	private final int worldY = -400;
	private final int worldWidth = 800;
	private final int worldHeight = 800;
	private final int worldZ = 100;
	private final int worldDepth = 800;

	Sphere sphere = new Sphere(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth, 25);
	
	Vector3D planePoint1 = new Vector3D(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth);
	Vector3D planePoint2 = new Vector3D(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth);
	Vector3D planePoint3 = new Vector3D(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth);

	private GL gl;

	public SpherePlaneTest(){

		canvas.addGLEventListener(this);
		gl = canvas.getGL();

		Animator animator = new FPSAnimator(canvas, 60);
		animator.start();

		JFrame frame = new JFrame("SpherePlaneTest");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.add(canvas, BorderLayout.CENTER);

		frame.setSize(1000, 1000);
		frame.setVisible(true);

		canvas.requestFocusInWindow();
	}


	public void init(GLAutoDrawable drawable) {

	}

	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();
		gl.glFrustum(-1, 1, -1, 1, 1, 1000);
		gl.glViewport(0, 0, width, height);
	}


	public void display(GLAutoDrawable drawable){

		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		
		gl.glColor4d(1, .5, .5, .5);
		
		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL);
		gl.glBegin(GL.GL_TRIANGLES);
		gl.glVertex3d(planePoint1.x, planePoint1.y, -planePoint1.z);
		gl.glVertex3d(planePoint2.x, planePoint2.y, -planePoint2.z);
		gl.glVertex3d(planePoint3.x, planePoint3.y, -planePoint3.z);
		gl.glEnd();

		sphere.step();
		sphere.drawSphere();

		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX, worldY, -worldZ);
		gl.glVertex3d(worldX+worldWidth, worldY, -worldZ);
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -worldZ);
		gl.glVertex3d(worldX, worldY+worldHeight, -worldZ);
		gl.glEnd();


		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX+worldWidth, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glVertex3d(worldX, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glEnd();

		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX, worldY, -(worldZ));
		gl.glVertex3d(worldX, worldY+worldHeight, -(worldZ));
		gl.glVertex3d(worldX, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glEnd();

		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX+worldWidth, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX+worldWidth, worldY, -(worldZ));
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -(worldZ));
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glEnd();
	}



	public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
	}


	private class Sphere{

		double maxSpeed = 5;
		Vector3D delta = new Vector3D(-maxSpeed+Math.random()*(maxSpeed*2), -maxSpeed+Math.random()*(maxSpeed*2), -maxSpeed+Math.random()*(maxSpeed*2));

		Vector3D centerPoint;

		float radius;

		public Sphere(double x, double y, double z, float radius){
			centerPoint = new Vector3D(x, y, z);
			this.radius = radius;
		}

		public void drawSphere(){
			gl.glColor3d(1, 1, 1);
			gl.glPointSize(radius);
			gl.glBegin(GL.GL_POINTS);
			gl.glVertex3d(centerPoint.x, centerPoint.y, -centerPoint.z);
			gl.glEnd();
		}

		public void step(){
			centerPoint = centerPoint.add(delta);
			
			if(DISTANCE_TO_PLANE < RADIUS){
				bounceOffPoint(POINT_CLOSEST_TO_PLANE);
			}
			
			if(centerPoint.x-radius < worldX){
				bounceOffPoint(new Vector3D(centerPoint.x-radius, centerPoint.y, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}
			else if(centerPoint.x+radius > worldX+worldWidth){
				bounceOffPoint(new Vector3D(centerPoint.x+radius, centerPoint.y, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}

			if(centerPoint.y-radius < worldY){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y-radius, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}
			else if(centerPoint.y+radius > worldX+worldHeight){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y+radius, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}

			if(centerPoint.z-radius < worldZ){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y, centerPoint.z-radius));
				centerPoint = centerPoint.add(delta);
			}
			else if(centerPoint.z+radius > worldZ+worldDepth){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y, centerPoint.z+radius));
				centerPoint = centerPoint.add(delta);
			}
		}

		public void bounceOffPoint(Vector3D point) {

			double deltaX =  point.x - centerPoint.x;
			double deltaY = point.y - centerPoint.y;
			double deltaZ = point.z - centerPoint.z;

			Vector3D around = new Vector3D(deltaX, deltaY, deltaZ).normalize();

			delta = delta.reverse().reflect(around);
		}
	}

	private static class Vector3D{
		private final double x;
		private final double y;
		private final double z;

		public Vector3D(double x, double y, double z){
			this.x = x;
			this.y = y;
			this.z = z;
		}


		public Vector3D add(Vector3D delta) {
			return new Vector3D(x+delta.x, y+delta.y, z+delta.z);
		}

		public Vector3D reflect(Vector3D around) {

			double reflectedX = 2 * this.dotProduct(around) * around.x - this.x;
			double reflectedY = 2 * this.dotProduct(around) * around.y - this.y;
			double reflectedZ = 2 * this.dotProduct(around) * around.z - this.z;

			return new Vector3D(reflectedX, reflectedY, reflectedZ);
		}

		public double dotProduct(Vector3D v2) {
			return (this.x * v2.x + this.y * v2.y + this.z * v2.z);
		}

		public Vector3D normalize(){
			double d = Math.sqrt(x * x + y * y + z * z);

			if (d == 0) {
				return this;
			}
			double normalizedX = x/d;
			double normalizedY = y/d;
			double normalizedZ = z/d;

			return new Vector3D(normalizedX, normalizedY, normalizedZ);
		}

		public Vector3D reverse(){
			return new Vector3D(-x, -y, -z);
		}

		private double distance(Vector3D other) {
			return Math.sqrt((x-other.x)*(x-other.x) + (y-other.y)*(y-other.y) + (z-other.z)*(z-other.z));
		}
	}

	public static void main(String[] args) {
		new SpherePlaneTest();
	}
}

The above program contains a sphere (a point) bouncing around a 3D box, as well as a 3D plane between 3 points. There is currently no interaction between the sphere and the plane, which is what I’m trying to add. Line 147 contains the basic logic I’m planning on using.

I’ve done a ton of googling on this, but I feel pretty stupid because a lot of it makes no sense to me. I’ve tried reading up on vector math, but it’s been quite some time since I’ve had linear algebra, so it seems that things other people consider obvious are pretty foreign to me. “Just use the crossproduct to get the normal and use ray tracing to calculate the intersection” might be correct, but I have yet to see a layman’s explanation of what any of that means in a real working program.

So I’m hoping somebody can show me how you might add a function that takes 4 3D points: 1 point at the center of the sphere and the other 3 defining a polygon face, and returns the closest point on that polygon face to that center point. I believe I can do the rest from there.

I also want to reiterate that the first part of this (the bouncing sphere) was homework (which I’ve finished), but the addition of the polygon intersection is outside of the bounds of the class and is mostly for my own curiosity. So nobody is going to be doing my homework for me or anything like that.

As always, I definitely appreciate any insights anybody can offer.

ja, i know what you mean. reading about such stuffs is sorta weird since others expect you to know the context they’re in, but forget to explain that.

i would simplify it a bit. i dont think we need the “closest point inside a triangle - to another point”. we want to know if a sphere intersects with a triangle right ?

there are many ways to get there, one could be :

  • convert triangel into plane (grab any vertex of triangle, grab normal of triangle)
  • compute closest point on the plane to source-point
  • test if distance to point is smaller (or equal) then radius of sphere
  • if - collision, if not - no collision.
  • test if computed point is inside triangle
  • if - collision, it not - no collision.

would that help ?

edit i think constraining a point inside a triangle is more cpu-hungry then this.

Thanks for the reply.

I’ve been trying to implement what you said, and I think I have the sphere bouncing off the infinite plane defined by the three points:

import java.awt.BorderLayout;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.swing.JFrame;

import com.sun.opengl.util.Animator;
import com.sun.opengl.util.FPSAnimator;

public class SpherePlaneTest implements GLEventListener {

	private final GLCanvas canvas = new GLCanvas();

	private final int worldX = -400;
	private final int worldY = -400;
	private final int worldWidth = 800;
	private final int worldHeight = 800;
	private final int worldZ = 100;
	private final int worldDepth = 800;

	Sphere sphere = new Sphere(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth, 25);

	Triangle triangle = new Triangle();

	private GL gl;

	public SpherePlaneTest(){

		canvas.addGLEventListener(this);
		gl = canvas.getGL();

		Animator animator = new FPSAnimator(canvas, 60);
		animator.start();

		JFrame frame = new JFrame("SpherePlaneTest");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.add(canvas, BorderLayout.CENTER);

		frame.setSize(1000, 1000);
		frame.setVisible(true);

		canvas.requestFocusInWindow();
	}


	public void init(GLAutoDrawable drawable) {

	}

	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();
		gl.glFrustum(-1, 1, -1, 1, 1, 1000);
		gl.glViewport(0, 0, width, height);
	}


	public void display(GLAutoDrawable drawable){

		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

		triangle.render();

		sphere.step();
		sphere.drawSphere();

		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX, worldY, -worldZ);
		gl.glVertex3d(worldX+worldWidth, worldY, -worldZ);
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -worldZ);
		gl.glVertex3d(worldX, worldY+worldHeight, -worldZ);
		gl.glEnd();


		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX+worldWidth, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glVertex3d(worldX, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glEnd();

		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX, worldY, -(worldZ));
		gl.glVertex3d(worldX, worldY+worldHeight, -(worldZ));
		gl.glVertex3d(worldX, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glEnd();

		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX+worldWidth, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX+worldWidth, worldY, -(worldZ));
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -(worldZ));
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glEnd();
	}



	public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
	}


	private class Sphere{

		double maxSpeed = 5;
		Vector3D delta = new Vector3D(-maxSpeed+Math.random()*(maxSpeed*2), -maxSpeed+Math.random()*(maxSpeed*2), -maxSpeed+Math.random()*(maxSpeed*2));

		Vector3D centerPoint;

		float radius;
	
		public Sphere(double x, double y, double z, float radius){
			centerPoint = new Vector3D(x, y, z);
			this.radius = radius;
		}

		public void drawSphere(){
			gl.glColor3d(1, 1, 1);
			gl.glPointSize(radius);
			gl.glBegin(GL.GL_POINTS);
			gl.glVertex3d(centerPoint.x, centerPoint.y, -centerPoint.z);
			gl.glEnd();
		}
		
		
		public void step(){
			centerPoint = centerPoint.add(delta);

			
			System.out.println(triangle.distanceToPoint(centerPoint));
			
			if(triangle.distanceToPoint(centerPoint) < radius){
				System.out.println("BOUNCING OFF PLANE");
				bounceAround(triangle.getNormal().normalize());
				centerPoint = centerPoint.add(delta);
			}

			if(centerPoint.x-radius < worldX){
				bounceOffPoint(new Vector3D(centerPoint.x-radius, centerPoint.y, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}
			else if(centerPoint.x+radius > worldX+worldWidth){
				bounceOffPoint(new Vector3D(centerPoint.x+radius, centerPoint.y, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}

			if(centerPoint.y-radius < worldY){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y-radius, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}
			else if(centerPoint.y+radius > worldX+worldHeight){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y+radius, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}

			if(centerPoint.z-radius < worldZ){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y, centerPoint.z-radius));
				centerPoint = centerPoint.add(delta);
			}
			else if(centerPoint.z+radius > worldZ+worldDepth){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y, centerPoint.z+radius));
				centerPoint = centerPoint.add(delta);
			}
		}

		public void bounceAround(Vector3D around){
			delta = delta.reverse().reflect(around);
		}
		
		public void bounceOffPoint(Vector3D point) {

			double deltaX =  point.x - centerPoint.x;
			double deltaY = point.y - centerPoint.y;
			double deltaZ = point.z - centerPoint.z;

			Vector3D around = new Vector3D(deltaX, deltaY, deltaZ).normalize();

			delta = delta.reverse().reflect(around);
		}
	}


	private class Triangle{
		Sphere planePoint1 = new Sphere(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth, 25);
		Sphere planePoint2 = new Sphere(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth, 25);
		Sphere planePoint3 = new Sphere(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth, 25);

		
		public Vector3D getNormal(){
			Vector3D edge1 = planePoint2.centerPoint.minus(planePoint1.centerPoint);
			Vector3D edge2 = planePoint3.centerPoint.minus(planePoint1.centerPoint);
			return edge1.crossProduct(edge2);
		}
		
		public double getD(){
			Vector3D normal = getNormal().normalize();
			return -normal.x*planePoint1.centerPoint.x -normal.y*planePoint1.centerPoint.y -normal.z*planePoint1.centerPoint.z;
		}
		
		public double distanceToPoint(Vector3D point){
			Vector3D normal = getNormal().normalize();
			Vector3D w = new Vector3D(planePoint1.centerPoint.x - point.x, planePoint1.centerPoint.y - point.y, planePoint1.centerPoint.z - point.z);
			double d = Math.abs(w.dotProduct(normal));
			return d;
		}
		
		
		public void render() {
			gl.glColor4d(1, .5, .5, .5);
			
			planePoint1.drawSphere();
			planePoint2.drawSphere();
			planePoint3.drawSphere();
			
			gl.glColor4d(1, .5, .5, .5);
			gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL);
			gl.glBegin(GL.GL_TRIANGLES);
			gl.glVertex3d(planePoint1.centerPoint.x, planePoint1.centerPoint.y, -planePoint1.centerPoint.z);
			gl.glVertex3d(planePoint2.centerPoint.x, planePoint2.centerPoint.y, -planePoint2.centerPoint.z);
			gl.glVertex3d(planePoint3.centerPoint.x, planePoint3.centerPoint.y, -planePoint3.centerPoint.z);
			gl.glEnd();
		}

	}

	private static class Vector3D{
		private final double x;
		private final double y;
		private final double z;

		public Vector3D(double x, double y, double z){
			this.x = x;
			this.y = y;
			this.z = z;
		}


		public Vector3D crossProduct(Vector3D other) {
			double cX = this.y*other.z - this.z*other.y;
			double cY = this.z*other.x - this.x*other.z;
			double cZ = this.x*other.y - this.y*other.x;
			return new Vector3D(cX, cY, cZ);
		}


		public Vector3D minus(Vector3D other) {
			return new Vector3D(x-other.x, y-other.y, z-other.z);
		}


		public Vector3D add(Vector3D delta) {
			return new Vector3D(x+delta.x, y+delta.y, z+delta.z);
		}

		public Vector3D reflect(Vector3D around) {

			double reflectedX = 2 * this.dotProduct(around) * around.x - this.x;
			double reflectedY = 2 * this.dotProduct(around) * around.y - this.y;
			double reflectedZ = 2 * this.dotProduct(around) * around.z - this.z;

			return new Vector3D(reflectedX, reflectedY, reflectedZ);
		}

		public double dotProduct(Vector3D v2) {
			return (this.x * v2.x + this.y * v2.y + this.z * v2.z);
		}

		public Vector3D normalize(){
			double d = Math.sqrt(x * x + y * y + z * z);

			if (d == 0) {
				return this;
			}
			double normalizedX = x/d;
			double normalizedY = y/d;
			double normalizedZ = z/d;

			return new Vector3D(normalizedX, normalizedY, normalizedZ);
		}

		public Vector3D reverse(){
			return new Vector3D(-x, -y, -z);
		}

		private double distance(Vector3D other) {
			return Math.sqrt((x-other.x)*(x-other.x) + (y-other.y)*(y-other.y) + (z-other.z)*(z-other.z));
		}
	}

	public static void main(String[] args) {
		new SpherePlaneTest();
	}
}

However, this doesn’t constrain the bouncing to the face of the triangle, so the sphere bounces off anything that’s on the plane. This is closer to what I want, but it’s still not what I’m actually looking for.

The only resources I can find are focused pretty heavily on the math, and don’t do a lot of explaining the logic behind that math. Can somebody point me to a good tutorial that explains this stuff from a pretty low level, or show me some code that does what I want?

I believe I’ve done that, in the getNormal() method of the Triangle class in the above code.

This is exactly what I’m trying to do. How would you go about this, specifically?

Right. Once I know the closes point on the plane, the rest is pretty easy. It’s getting that point that’s giving me so much trouble.

I think this is where I’m currently stuck. I think I can get the distance to the infinite plane. How would I calculate the closest point? And once I have that, how would I test if it’s inside the triangle?

[quote]The only resources I can find are focused pretty heavily on the math, and don’t do a lot of explaining the logic behind that math.
[/quote]
amen. tho’ i suck at explaining the logic by myself.

i hope you dont mind glsl-like-pseudocode. i think it is easier to understand :slight_smile: … easy to rewrite it into your own style at least. i did not run your code :open_mouth: and my additions may contain bugs :o

* convert triangle to plane :

there are at least two valid ways to describe a plane. one, that is usually not used is :

class plane {

  vec3 point;
  vec3 normal;

}

that comes naturally if you grab any vertex from the triangle and it’s normal.

* compute closest point

projecting a point on it can be done like

vec3 project(plane plane, vec3 v)
{
  float dot = dot(plane.normal, v - plane.point);
  vec3 d = plane.normal * dot;
  return v + d;
}

[quote=“KevinWorkman,post:3,topic:51130”]
aye. testing the distance on this to the sphere can be done cheap when we compare the square-distance to spheres square-radius.

i believe too that is what you already did :). looks like half on a ray->plane intersection while the missing part is 1.0 anyway.

all this is very similar to what you compute when doing “regular” raytracing.

* test if point is inside a triange :

… can be done in different ways too. one is to convert the triangle into barycentric coords :

assuming a triangle is something like

class triangle {

  vec3 a;
  vec3 b;
  vec3 c;

}

test could be

boolean contains(triangle t, vec3 p)
{
  vec3 v0 = t.c - t.a; // like "edges" when computing triangle normal
  vec3 v1 = t.b - t.a;

  vec3 v2 = p - t.a;

  float dot00 = dot(v0, v0);
  float dot01 = dot(v0, v1);
  float dot02 = dot(v0, v2);
  float dot11 = dot(v1, v1);
  float dot12 = dot(v1, v2);

  // barycentric coordinates
  float invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01);
  float u = (dot11 * dot02 - dot01 * dot12) * invDenom;
  float v = (dot00 * dot12 - dot01 * dot02) * invDenom;

  return (u >= 0.0) && (v >= 0.0) && (u + v <= 1.0);
}

another way is not using barycentric but something like this : http://www.graphics.cornell.edu/pubs/1997/MT97.pdf
it is a tiny bit faster, but not as easy to understand (still worth if you cast tons of rays).

if you get it to work like that, you will see that some values could be reused, or the whole thing could be computed in one go … or it could be optimised in other ways. then it’s possible we might end up with not-so-readable code anymore … but 0.01% faster.

hope that helps :slight_smile:

Huzzah! I finally got it working thanks to you. Thanks for being so understanding; I owe you a beer or something!

Here is the code I came up with, it seems to work, but any further comments or suggestions for improvement are welcome:

import java.awt.BorderLayout;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.swing.JFrame;

import com.sun.opengl.util.Animator;
import com.sun.opengl.util.FPSAnimator;

public class SpherePlaneTest implements GLEventListener {

	private final GLCanvas canvas = new GLCanvas();

	private final int worldX = -400;
	private final int worldY = -400;
	private final int worldWidth = 800;
	private final int worldHeight = 800;
	private final int worldZ = 100;
	private final int worldDepth = 800;

	Sphere sphere = new Sphere(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth, 25);

	Triangle triangle = new Triangle();

	private GL gl;

	public SpherePlaneTest(){

		canvas.addGLEventListener(this);
		gl = canvas.getGL();

		Animator animator = new FPSAnimator(canvas, 60);
		animator.start();

		JFrame frame = new JFrame("SpherePlaneTest");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.add(canvas, BorderLayout.CENTER);

		frame.setSize(1000, 1000);
		frame.setVisible(true);

		canvas.requestFocusInWindow();
	}


	public void init(GLAutoDrawable drawable) {

	}

	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glLoadIdentity();
		gl.glFrustum(-1.25, 1.25, -1.25, 1.25, 1, 1000);
		gl.glViewport(0, 0, width, height);
	}


	public void display(GLAutoDrawable drawable){

		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

		triangle.render();

		sphere.step();
		sphere.drawSphere();

		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX, worldY, -worldZ);
		gl.glVertex3d(worldX+worldWidth, worldY, -worldZ);
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -worldZ);
		gl.glVertex3d(worldX, worldY+worldHeight, -worldZ);
		gl.glEnd();


		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX+worldWidth, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glVertex3d(worldX, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glEnd();

		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX, worldY, -(worldZ));
		gl.glVertex3d(worldX, worldY+worldHeight, -(worldZ));
		gl.glVertex3d(worldX, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glEnd();

		gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE);
		gl.glColor3f(1f, 1f, 1f);
		gl.glBegin(GL.GL_POLYGON);
		gl.glVertex3d(worldX+worldWidth, worldY, -(worldZ+worldDepth));
		gl.glVertex3d(worldX+worldWidth, worldY, -(worldZ));
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -(worldZ));
		gl.glVertex3d(worldX+worldWidth, worldY+worldHeight, -(worldZ+worldDepth));
		gl.glEnd();
	}



	public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
	}


	private class Sphere{

		double maxSpeed = 5;
		Vector3D delta = new Vector3D(-maxSpeed+Math.random()*(maxSpeed*2), -maxSpeed+Math.random()*(maxSpeed*2), -maxSpeed+Math.random()*(maxSpeed*2));

		Vector3D centerPoint;


		float radius;
		

	
		public Sphere(double x, double y, double z, float radius){
			centerPoint = new Vector3D(x, y, z);
			this.radius = radius;
		}

		public void drawSphere(){
			gl.glColor3d(1, 1, 1);
			gl.glPointSize(radius);
			gl.glBegin(GL.GL_POINTS);
			gl.glVertex3d(centerPoint.x, centerPoint.y, -centerPoint.z);
			gl.glEnd();
		}
		
		
		public void step(){
			centerPoint = centerPoint.add(delta);

			
			if(triangle.closestPointOnPlane(centerPoint).distance(centerPoint) < radius){
				if(triangle.contains(triangle.closestPointOnPlane(centerPoint))){
					System.out.println("BOUNCING OFF PLANE");
					bounceAround(triangle.getNormal().normalize());
					
					
					while(triangle.closestPointOnPlane(centerPoint).distance(centerPoint) < radius &&triangle.contains(triangle.closestPointOnPlane(centerPoint))){
						centerPoint = centerPoint.add(delta);
					}
				}
			}

			if(centerPoint.x-radius < worldX){
				bounceOffPoint(new Vector3D(centerPoint.x-radius, centerPoint.y, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}
			else if(centerPoint.x+radius > worldX+worldWidth){
				bounceOffPoint(new Vector3D(centerPoint.x+radius, centerPoint.y, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}

			if(centerPoint.y-radius < worldY){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y-radius, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}
			else if(centerPoint.y+radius > worldX+worldHeight){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y+radius, centerPoint.z));
				centerPoint = centerPoint.add(delta);
			}

			if(centerPoint.z-radius < worldZ){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y, centerPoint.z-radius));
				centerPoint = centerPoint.add(delta);
			}
			else if(centerPoint.z+radius > worldZ+worldDepth){
				bounceOffPoint(new Vector3D(centerPoint.x, centerPoint.y, centerPoint.z+radius));
				centerPoint = centerPoint.add(delta);
			}
		}

		public void bounceAround(Vector3D around){
			delta = delta.reverse().reflect(around);
		}
		
		public void bounceOffPoint(Vector3D point) {

			double deltaX =  point.x - centerPoint.x;
			double deltaY = point.y - centerPoint.y;
			double deltaZ = point.z - centerPoint.z;

			Vector3D around = new Vector3D(deltaX, deltaY, deltaZ).normalize();

			delta = delta.reverse().reflect(around);
		}
	}


	private class Triangle{
		Sphere planePoint1 = new Sphere(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth, 25);
		Sphere planePoint2 = new Sphere(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth, 25);
		Sphere planePoint3 = new Sphere(worldX + Math.random() * worldWidth, worldY + Math.random() * worldHeight, worldZ + Math.random() * worldDepth, 25);

		
		public Vector3D getNormal(){
			Vector3D edge1 = planePoint2.centerPoint.minus(planePoint1.centerPoint);
			Vector3D edge2 = planePoint3.centerPoint.minus(planePoint1.centerPoint);
			return edge1.crossProduct(edge2);
		}
		

		public Vector3D closestPointOnPlane(Vector3D point){

			 //float dot = dot(plane.normal, v - plane.point);
			double dot = getNormal().normalize().dotProduct(point.minus(planePoint1.centerPoint));
			
			//vec3 d = plane.normal * dot;
			Vector3D d = getNormal().normalize().timesScalar(dot);
			
			//return v + d;
			return point.add(d);
		}
		
		public boolean contains(Vector3D p)
		{
		  Vector3D v0 = planePoint3.centerPoint.minus(planePoint1.centerPoint); // like "edges" when computing triangle normal
		  Vector3D v1 = planePoint2.centerPoint.minus(planePoint1.centerPoint);

		  Vector3D v2 = p.minus(planePoint1.centerPoint);

		  double dot00 = v0.dotProduct(v0);
		  double dot01 = v0.dotProduct(v1);
		  double dot02 = v0.dotProduct(v2);
		  double dot11 = v1.dotProduct(v1);
		  double dot12 = v1.dotProduct(v2);

		  // barycentric coordinates
		  double invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01);
		  double u = (dot11 * dot02 - dot01 * dot12) * invDenom;
		  double v = (dot00 * dot12 - dot01 * dot02) * invDenom;

		  return (u >= 0.0) && (v >= 0.0) && (u + v <= 1.0);
		}
		
		
		public void render() {
			gl.glColor4d(1, .5, .5, .5);
			
			planePoint1.drawSphere();
			planePoint2.drawSphere();
			planePoint3.drawSphere();
			
			gl.glColor4d(1, .5, .5, .5);
			gl.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL);
			gl.glBegin(GL.GL_TRIANGLES);
			gl.glVertex3d(planePoint1.centerPoint.x, planePoint1.centerPoint.y, -planePoint1.centerPoint.z);
			gl.glVertex3d(planePoint2.centerPoint.x, planePoint2.centerPoint.y, -planePoint2.centerPoint.z);
			gl.glVertex3d(planePoint3.centerPoint.x, planePoint3.centerPoint.y, -planePoint3.centerPoint.z);
			gl.glEnd();
		}

	}

	private static class Vector3D{
		private final double x;
		private final double y;
		private final double z;

		public Vector3D(double x, double y, double z){
			this.x = x;
			this.y = y;
			this.z = z;
		}


		public Vector3D timesScalar(double s) {
			return new Vector3D(x*s, y*s, z*s);
		}


		public Vector3D crossProduct(Vector3D other) {
			double cX = this.y*other.z - this.z*other.y;
			double cY = this.z*other.x - this.x*other.z;
			double cZ = this.x*other.y - this.y*other.x;
			return new Vector3D(cX, cY, cZ);
		}


		public Vector3D minus(Vector3D other) {
			return new Vector3D(x-other.x, y-other.y, z-other.z);
		}


		public Vector3D add(Vector3D delta) {
			return new Vector3D(x+delta.x, y+delta.y, z+delta.z);
		}

		public Vector3D reflect(Vector3D around) {

			double reflectedX = 2 * this.dotProduct(around) * around.x - this.x;
			double reflectedY = 2 * this.dotProduct(around) * around.y - this.y;
			double reflectedZ = 2 * this.dotProduct(around) * around.z - this.z;

			return new Vector3D(reflectedX, reflectedY, reflectedZ);
		}

		public double dotProduct(Vector3D v2) {
			return (this.x * v2.x + this.y * v2.y + this.z * v2.z);
		}

		public Vector3D normalize(){
			double d = Math.sqrt(x * x + y * y + z * z);

			if (d == 0) {
				return this;
			}
			double normalizedX = x/d;
			double normalizedY = y/d;
			double normalizedZ = z/d;

			return new Vector3D(normalizedX, normalizedY, normalizedZ);
		}

		public Vector3D reverse(){
			return new Vector3D(-x, -y, -z);
		}

		private double distance(Vector3D other) {
			return Math.sqrt((x-other.x)*(x-other.x) + (y-other.y)*(y-other.y) + (z-other.z)*(z-other.z));
		}
	}

	public static void main(String[] args) {
		new SpherePlaneTest();
	}
}

awesome sauce ! ;D