No pun intended
I want to post how this is (easily) done, since I can't find any useful examples online. I keep needing this on and off over the years, so I might was well give it a home here.
It's actually super easy to do, but there are very few discussions of such. It's also the same as checking the distance between a point and a plane (since a sphere is just a point and a distance).
First, take a three points to define the plane and determine the normal to the plane using these points.
For reference:
function cross(a, b){
let x = (a.y * b.z) - (a.z * b.y);
let y = (a.z * b.x) - (a.x * b.z);
let z = (a.x * b.y) - (a.y * b.x);
return {"x":x, "y":y, "z":z};
}
function subtract(a, b){
return {"x":a.x - b.x, "y":a.y - b.y, "z":a.z - b.z};
}
function normal(a, b, c){
let da = subtract(a, c);
let db = subtract(b, c);
return cross(da, db);
}
Then, determine the plane's D component which is negative of the normal multiplied by one of the points.
function multiply(a, b){
return {"x":a.x * b.x, "y":a.y * b.y, "z":a.z * b.z};
}
function plane_d(normal_cmp, point){
let d_vec = subtract({"x":0, "y":0, "z":0}, multiply(normal_cmp, point));
return d_vec.x + d_vec.y + d_vec.z;
}
So, to create a plane (we will want the normal vector to be normalized for later):
function normalize(vec){
let m = Math.sqrt((vec.x * vec.x) + (vec.y * vec.y) + (vec.z * vec.z));
return { "x":vec.x/m, "y":vec.y/m, "z":vec.z/m };
}
function plane(a, b, c){
this.points = [a, b, c]; // It will be useful to know at least one point on the plane later.
this.normal = normalize(normal(a, b, c));
this.d = plane_d(this.normal, a);
}
Alright, so the very obvious part. We need to collide a line that passes through the sphere's center, from the points on the sphere's edge that are exactly the radius away in both the plane's normal and anti-normal (aka, a line going all the way through the sphere, crossing the center, and perpendicular to the plane).
Then, we generate a 3x3 matrix from this and the difference of the three points we defined the plane from (any three points on the plane would work, but here we already have some). Then, invert the matrix and multiply that by the matrix [ line.x-points[0].x, line.y - points[0].y, line.z - points[0].z ] (but in column order).
/*
* [ a b c ] [ A D G ] [ (ei-fh) -(bi-ch) (bf-ce) ]
* P [ d e f ] = det(A) [ B E H ] = det(A) [ -(di-fg) (ai-cg) -(af-cd) ]
* [ g h i ] [ C F I ] [ (dh-eg) -(ab-bg) (ae-bd) ]
* For the line of points A and B and the plane of points 0, 1, 2
* [ (ei-fh) -(bi-ch) (bf-ce) ] | [ (Xa-Xb) (X1-X0) (X2-X0) ]
* det(A) [ -(di-fg) (ai-cg) -(af-cd) ] | [ (Ya-Yb) (Y1-Y0) (Y2-Y0) ] = W
* [ (dh-eg) -(ab-bg) (ae-bd) ] | [ (Za-Zb) (Z1-Z0) (Z2-Z0) ]
*
* det(A) = 1/(a * A + b * B + c * C)
* det(A) = 1/((Xa-Xb) * (ei-fh) + (X1-X0) * (ch-bi) + (X2-X0) * (bf-ce))
* [ Xa-X0 ] [ T ]
* Thus, inverse W * [ Ya-Y0 ] = [ U ]
* [ Za-Z0 ] [ V ]
* T is between 0 and 1 if the intersection is along the line.
* (U+V) <= 1 if the intersection is withing the three points.
*/
plane.prototype.whyareyoudoingthistome = function(sphere){
line = {"x1":sphere.x + this.normal.x * sphere.radius, "y1":sphere.y + this.normal.y * sphere.radius, "z1":sphere.z + this.normal.z * sphere.radius,
"x2":sphere.x - this.normal.x * sphere.radius, "y2":sphere.y - this.normal.y * sphere.radius, "z2":sphere.z - this.normal.z * sphere.radius};
let a = line.x1 - line.x2, b = this.points[1].x - this.points[0].x, c = this.points[2].x - this.points[0].x,
d = line.y1 - line.y2, e = this.points[1].y - this.points[0].y, f = this.points[2].y - this.points[0].y,
g = line.z1 - line.z2, e = this.points[1].z - this.points[0].z, f = this.points[2].z - this.points[0].z;
// The commented out portions would only be useful if we wanted to determine if the intersection was bound by the three input points.
let A = ((e*i)-(f*h)), B = ((c*h)-(b*i)), C = ((b*f)-(c*e)),
D = ((f*g)-(d*i)),/* E = ((a*i)-(c*g)), F = ((c*d)-(a*f)), */
G = ((d*h)-(e*g)) /* H = ((b*g)-(a*b)), I = ((a*e)-(b*d)) */ ;
let Q = line.x1 - this.points[0].x, R = line.y1 - this.points[0].y, S = line.z1 - this.points[0].z;
// Inversion coefficient, of course. Mismatched to match the final matrix rotation for inversion.
let det = 1 / ((a*A) + (b*D) + (c*G));
let T = ((A*Q)+(B*R)+(C*S)) * det;
// U and V are UV coordinates on the triangle along which we intersected. If you were ray-tracing, for instance, this would be texture coordinates relative to the intersection.
// let U = (float)((D*R)+(E*S)+(F*T)) * det_a;
// let V = (float)((G*R)+(H*S)+(I*T)) * det_a;
// T is actually the T position along the line where the plane and the line intersect.
return T >= 0 && T <= 1;
}
So obvious, so clear.
Except this is actually the
really hard way. I've done it this way for some years, but today I made a discovery that made me feel like an idiot.
Are you ready for it?
Here is the easy wayThe D coordinate of the plane's equation, if the normal vector has been normalized before D was calculated, is the distance from the point to the plane.
plane.prototype.collideSphere(sphere){
let point = multiply(sphere, this.normal);
let distance1 = point.x + point.y + point.z + this.d;
return Math.abs(distance1 ) < sphere.radius;
}
...
Sometimes I feel that my calculus classes in college let me down me in some ways.