I know all coordinates of tetrahedron and the point I would like to determine. So does anyone know how to do it? I've tried to determine the point's belonging to each triangle of tetrahedron, and if it's true to all triangles then the point is in the tetrahedron. But it's absolutely wrong.
For each plane of the tetrahedron, check if the point is on the same side as the remaining vertex:
bool SameSide(v1, v2, v3, v4, p)
{
normal := cross(v2 - v1, v3 - v1)
dotV4 := dot(normal, v4 - v1)
dotP := dot(normal, p - v1)
return Math.Sign(dotV4) == Math.Sign(dotP);
}
And you need to check this for each plane:
bool PointInTetrahedron(v1, v2, v3, v4, p)
{
return SameSide(v1, v2, v3, v4, p) &&
SameSide(v2, v3, v4, v1, p) &&
SameSide(v3, v4, v1, v2, p) &&
SameSide(v4, v1, v2, v3, p);
}
You define a tetrahedron by four vertices, A B C and D.
Therefore you also can have the 4 triangles defining the surface of the tetrahedron.
You now just check if a point P is on the other side of the plane. The normal of each plane is pointing away from the center of the tetrahedron.
So you just have to test against 4 planes.
Your plane equation looks like this: a*x+b*y+c*z+d=0 Just fill in the point values (x y z). If the sign of the result is >0 the point is of the same side as the normal, result == 0, point lies in the plane, and in your case you want the third option: <0 means it is on the backside of the plane.
If this is fulfilled for all 4 planes, your point lies inside the tetrahedron.
Starting from Hugues' solution, here is a simpler and (even) more efficient one:
import numpy as np
def tetraCoord(A,B,C,D):
# Almost the same as Hugues' function,
# except it does not involve the homogeneous coordinates.
v1 = B-A ; v2 = C-A ; v3 = D-A
mat = np.array((v1,v2,v3)).T
# mat is 3x3 here
M1 = np.linalg.inv(mat)
return(M1)
def pointInside(v1,v2,v3,v4,p):
# Find the transform matrix from orthogonal to tetrahedron system
M1=tetraCoord(v1,v2,v3,v4)
# apply the transform to P (v1 is the origin)
newp = M1.dot(p-v1)
# perform test
return (np.all(newp>=0) and np.all(newp <=1) and np.sum(newp)<=1)
In the coordinate system associated to the tetrahedron, the opposite face from the origin (denoted v1 here) is characterized by x+y+z=1. Thus, the half space on the same side as v1 from this face satisfies x+y+z<1.
As a comparison, here is the full code for comparing the methods proposed by Nico, Hugues and me:
import numpy as np
import time
def sameside(v1,v2,v3,v4,p):
normal = np.cross(v2-v1, v3-v1)
return (np.dot(normal, v4-v1) * np.dot(normal, p-v1) > 0)
# Nico's solution
def pointInside_Nico(v1,v2,v3,v4,p):
return sameside(v1, v2, v3, v4, p) and sameside(v2, v3, v4, v1, p) and sameside(v3, v4, v1, v2, p) and sameside(v4, v1, v2, v3, p)
# Hugues' solution
def tetraCoord(A,B,C,D):
v1 = B-A ; v2 = C-A ; v3 = D-A
# mat defines an affine transform from the tetrahedron to the orthogonal system
mat = np.concatenate((np.array((v1,v2,v3,A)).T, np.array([[0,0,0,1]])))
# The inverse matrix does the opposite (from orthogonal to tetrahedron)
M1 = np.linalg.inv(mat)
return(M1)
def pointInside_Hugues(v1,v2,v3,v4,p):
# Find the transform matrix from orthogonal to tetrahedron system
M1=tetraCoord(v1,v2,v3,v4)
# apply the transform to P
p1 = np.append(p,1)
newp = M1.dot(p1)
# perform test
return(np.all(newp>=0) and np.all(newp <=1) and sameside(v2,v3,v4,v1,p))
# Proposed solution
def tetraCoord_Dorian(A,B,C,D):
v1 = B-A ; v2 = C-A ; v3 = D-A
# mat defines an affine transform from the tetrahedron to the orthogonal system
mat = np.array((v1,v2,v3)).T
# The inverse matrix does the opposite (from orthogonal to tetrahedron)
M1 = np.linalg.inv(mat)
return(M1)
def pointInside_Dorian(v1,v2,v3,v4,p):
# Find the transform matrix from orthogonal to tetrahedron system
M1=tetraCoord_Dorian(v1,v2,v3,v4)
# apply the transform to P
newp = M1.dot(p-v1)
# perform test
return (np.all(newp>=0) and np.all(newp <=1) and np.sum(newp)<=1)
npt=100000
Pt=np.random.rand(npt,3)
A=np.array([0.1, 0.1, 0.1])
B=np.array([0.9, 0.2, 0.1])
C=np.array([0.1, 0.9, 0.2])
D=np.array([0.3, 0.3, 0.9])
inTet_Nico=np.zeros(shape=(npt,1),dtype=bool)
inTet_Hugues=inTet_Nico
inTet_Dorian=inTet_Nico
start_time = time.time()
for i in range(0,npt):
inTet_Nico[i]=pointInside_Nico(A,B,C,D,Pt[i,:])
print("--- %s seconds ---" % (time.time() - start_time)) # https://stackoverflow.com/questions/1557571/how-do-i-get-time-of-a-python-programs-execution
start_time = time.time()
for i in range(0,npt):
inTet_Hugues[i]=pointInside_Hugues(A,B,C,D,Pt[i,:])
print("--- %s seconds ---" % (time.time() - start_time))
start_time = time.time()
for i in range(0,npt):
inTet_Dorian[i]=pointInside_Dorian(A,B,C,D,Pt[i,:])
print("--- %s seconds ---" % (time.time() - start_time))
And here are the results, in terms of running time:
--- 15.621951341629028 seconds ---
--- 8.97989797592163 seconds ---
--- 4.597853660583496 seconds ---
[EDIT]
Based on the Tom's idea of vectorizing the process, if one wants to find which element of a mesh contains a given point, here is a somehow highly vectorized solution:
Input data:
node_coordinates: (n_nodes,3) array containing the coordinates of each node
node_ids: (n_tet, 4) array, where the i-th row gives the vertex indices of the i-th tetrahedron.
def where(node_coordinates, node_ids, p):
ori=node_coordinates[node_ids[:,0],:]
v1=node_coordinates[node_ids[:,1],:]-ori
v2=node_coordinates[node_ids[:,2],:]-ori
v3=node_coordinates[node_ids[:,3],:]-ori
n_tet=len(node_ids)
v1r=v1.T.reshape((3,1,n_tet))
v2r=v2.T.reshape((3,1,n_tet))
v3r=v3.T.reshape((3,1,n_tet))
mat = np.concatenate((v1r,v2r,v3r), axis=1)
inv_mat = np.linalg.inv(mat.T).T # https://stackoverflow.com/a/41851137/12056867
if p.size==3:
p=p.reshape((1,3))
n_p=p.shape[0]
orir=np.repeat(ori[:,:,np.newaxis], n_p, axis=2)
newp=np.einsum('imk,kmj->kij',inv_mat,p.T-orir)
val=np.all(newp>=0, axis=1) & np.all(newp <=1, axis=1) & (np.sum(newp, axis=1)<=1)
id_tet, id_p = np.nonzero(val)
res = -np.ones(n_p, dtype=id_tet.dtype) # Sentinel value
res[id_p]=id_tet
return res
The hack here is to do the matrix product with multidimensional arrays.
The where function takes the point(s) coordinates as the 3rd argument. Indeed, this function can be run on multiple coordinates at once; the output argument is of same length as p. It returns -1 if the corresponding coordinate is not in the mesh.
On a mesh consisting in 1235 tetrahedrons, this method is 170-180 times faster than looping over each tetrahedron. Such a mesh is very small, so this gap may increase for larger meshes.
Given 4 points A,B,C,D defining a non-degenerate tetrahedron, and a point P to test, one way would be to transform the coordinates of P into the tetrahedron coordinate system, for example taking A as the origin, and the vectors B-A, C-A, D-A as the unit vectors.
In this coordinate system, the coordinates of P are all between 0 and 1 if it is inside P, but it could also be in anywhere in the transformed cube defined by the origin and the 3 unit vectors.
One way to assert that P is inside (A,B,C,D) is by taking in turn as origin the points (A, B, C, and D) and the other three points to define a new coordinate system. This test repeated 4 times is effective but can be improved.
It is most efficient to transform the coordinates only once and reuse the SameSide function as proposed earlier, for example taking A as the origin, transforming into the (A,B,C,D) coordinates system, P and A must lie on the same side of the (B,C,D) plane.
Following is a numpy/python implementation of that test. Tests indicate this method is 2-3 times faster than the Planes method.
import numpy as np
def sameside(v1,v2,v3,v4,p):
normal = np.cross(v2-v1, v3-v1)
return ((np.dot(normal, v4-v1)*p.dot(normal, p-v1) > 0)
def tetraCoord(A,B,C,D):
v1 = B-A ; v2 = C-A ; v3 = D-A
# mat defines an affine transform from the tetrahedron to the orthogonal system
mat = np.concatenate((np.array((v1,v2,v3,A)).T, np.array([[0,0,0,1]])))
# The inverse matrix does the opposite (from orthogonal to tetrahedron)
M1 = np.linalg.inv(mat)
return(M1)
def pointInsideT(v1,v2,v3,v4,p):
# Find the transform matrix from orthogonal to tetrahedron system
M1=tetraCoord(v1,v2,v3,v4)
# apply the transform to P
p1 = np.append(p,1)
newp = M1.dot(p1)
# perform test
return(np.all(newp>=0) and np.all(newp <=1) and sameside(v2,v3,v4,v1,p))
I've vectorized Dorian and Hughes solutions to take the entire array of points as input. I also moved the tetraCoord function outside of the pointsInside function and renamed both, since there was no point in calling it for every point.
On my computer, #Dorian's solution and example runs in 2.5 seconds. On the same data, mine runs nearly a thousand times faster at 0.003 seconds. If one for some reason needs even more speed, importing the GPU cupy package as "np" pushes it into the 100 microsecond range.
import time
# alternatively, import cupy as np if len(points)>1e7 and GPU
import numpy as np
def Tetrahedron(vertices):
"""
Given a list of the xyz coordinates of the vertices of a tetrahedron,
return tetrahedron coordinate system
"""
origin, *rest = vertices
mat = (np.array(rest) - origin).T
tetra = np.linalg.inv(mat)
return tetra, origin
def pointInside(point, tetra, origin):
"""
Takes a single point or array of points, as well as tetra and origin objects returned by
the Tetrahedron function.
Returns a boolean or boolean array indicating whether the point is inside the tetrahedron.
"""
newp = np.matmul(tetra, (point-origin).T).T
return np.all(newp>=0, axis=-1) & np.all(newp <=1, axis=-1) & (np.sum(newp, axis=-1) <=1)
npt=10000000
points = np.random.rand(npt,3)
# Coordinates of vertices A, B, C and D
A=np.array([0.1, 0.1, 0.1])
B=np.array([0.9, 0.2, 0.1])
C=np.array([0.1, 0.9, 0.2])
D=np.array([0.3, 0.3, 0.9])
start_time = time.time()
vertices = [A, B, C, D]
tetra, origin = Tetrahedron(vertices)
inTet = pointInside(points, tetra, origin)
print("--- %s seconds ---" % (time.time() - start_time))
Thanks to Dorian's test case script i could work on yet another solution and compare it quickly to the ones so far.
the intuition
for a triangle ABC and a point P if one connects P to the corners to get the vectors PA, PB, PC and compares the two triangles X and Y that are spanned by PA,PC and PB,PC, then the point P lies within the triangle ABC if X and Y overlap.
Or in other words, it is impossible to construct the vector PA by linearly combining PC and PB with only positive coefficients if P is in the triangle ABC.
From there on i tried to transfer it to the tetrahedron case and read here that it is possible to check whether vectors are linearly independent by checking the determinant of the matrix constructed out of the vectors as columns to be non-zero.
I tried out various approaches using determinants and i stumbled across this one:
Let PA, PB, PC, PD be the connections of P to the tetrahedron points ABCD (i.e. PA = A - P, etc.). calculate the determinants detA = det(PB PC PD), detB, detC and detD (like detA).
Then the point P lies within the tetrahedron spanned by ABCD if:
detA > 0 and detB < 0 and detC > 0 and detD < 0
or
detA < 0 and detB > 0 and detC < 0 and detD > 0
so the determinants switch signs, starting from negative, or starting from positive.
Does it work ? Apparently. Why does it work ? I don't know, or at least, i can't proof it. Maybe someone else with better math skills can help us here.
(EDIT: actually barycentric coordinates can be defined using those determinants and in the end, the barycentric coordinates need to sum up to one. It is like comparing the volumes of the tetrahedra that are spanned by the combinations of P with the points A,B,C,D with the volume of the tetrahedron ABCD itself. The explained case with observing the determinant signs is still unclear whether it works in general and i don't recommend it)
i changed the test case to not check n points Pi against one tetrahedron T, but to check n points Pi against n tetrahedrons Ti. All of the answers still give correct results. I think the reason why this approach is faster is that it doesn't need a matrix inversion.
I left TomNorway's approach implemented with one tetrahedron, and i leave the vectorization of this new approach to others since i am not so familiar with python and numpy.
import numpy as np
import time
def sameside(v1,v2,v3,v4,p):
normal = np.cross(v2-v1, v3-v1)
return (np.dot(normal, v4-v1) * np.dot(normal, p-v1) > 0)
# Nico's solution
def pointInside_Nico(v1,v2,v3,v4,p):
return sameside(v1, v2, v3, v4, p) and sameside(v2, v3, v4, v1, p) and sameside(v3, v4, v1, v2, p) and sameside(v4, v1, v2, v3, p)
# Hugues' solution
def tetraCoord(A,B,C,D):
v1 = B-A ; v2 = C-A ; v3 = D-A
# mat defines an affine transform from the tetrahedron to the orthogonal system
mat = np.concatenate((np.array((v1,v2,v3,A)).T, np.array([[0,0,0,1]])))
# The inverse matrix does the opposite (from orthogonal to tetrahedron)
M1 = np.linalg.inv(mat)
return(M1)
def pointInside_Hugues(v1,v2,v3,v4,p):
# Find the transform matrix from orthogonal to tetrahedron system
M1=tetraCoord(v1,v2,v3,v4)
# apply the transform to P
p1 = np.append(p,1)
newp = M1.dot(p1)
# perform test
return(np.all(newp>=0) and np.all(newp <=1) and sameside(v2,v3,v4,v1,p))
#Dorian's solution
def tetraCoord_Dorian(A,B,C,D):
v1 = B-A ; v2 = C-A ; v3 = D-A
# mat defines an affine transform from the tetrahedron to the orthogonal system
mat = np.array((v1,v2,v3)).T
# The inverse matrix does the opposite (from orthogonal to tetrahedron)
M1 = np.linalg.inv(mat)
return(M1)
def pointInside_Dorian(v1,v2,v3,v4,p):
# Find the transform matrix from orthogonal to tetrahedron system
M1=tetraCoord_Dorian(v1,v2,v3,v4)
# apply the transform to P
newp = M1.dot(p-v1)
# perform test
return (np.all(newp>=0) and np.all(newp <=1) and np.sum(newp)<=1)
#TomNorway's solution adapted to cope with n tetrahedrons
def Tetrahedron(vertices):
"""
Given a list of the xyz coordinates of the vertices of a tetrahedron,
return tetrahedron coordinate system
"""
origin, *rest = vertices
mat = (np.array(rest) - origin).T
tetra = np.linalg.inv(mat)
return tetra, origin
def pointInside(point, tetra, origin):
"""
Takes a single point or array of points, as well as tetra and origin objects returned by
the Tetrahedron function.
Returns a boolean or boolean array indicating whether the point is inside the tetrahedron.
"""
newp = np.matmul(tetra, (point-origin).T).T
return np.all(newp>=0, axis=-1) & np.all(newp <=1, axis=-1) & (np.sum(newp, axis=-1) <=1)
# Proposed solution
def det3x3_Philipp(b,c,d):
return b[0]*c[1]*d[2] + c[0]*d[1]*b[2] + d[0]*b[1]*c[2] - d[0]*c[1]*b[2] - c[0]*b[1]*d[2] - b[0]*d[1]*c[2]
def pointInside_Philipp(v0,v1,v2,v3,p):
a = v0 - p
b = v1 - p
c = v2 - p
d = v3 - p
detA = det3x3_Philipp(b,c,d)
detB = det3x3_Philipp(a,c,d)
detC = det3x3_Philipp(a,b,d)
detD = det3x3_Philipp(a,b,c)
ret0 = detA > 0.0 and detB < 0.0 and detC > 0.0 and detD < 0.0
ret1 = detA < 0.0 and detB > 0.0 and detC < 0.0 and detD > 0.0
return ret0 or ret1
npt=100000
Pt= np.array([ np.array([p[0]-0.5,p[1]-0.5,p[2]-0.5]) for p in np.random.rand(npt,3)])
A=np.array([ np.array([p[0]-0.5,p[1]-0.5,p[2]-0.5]) for p in np.random.rand(npt,3)])
B=np.array([ np.array([p[0]-0.5,p[1]-0.5,p[2]-0.5]) for p in np.random.rand(npt,3)])
C=np.array([ np.array([p[0]-0.5,p[1]-0.5,p[2]-0.5]) for p in np.random.rand(npt,3)])
D=np.array([ np.array([p[0]-0.5,p[1]-0.5,p[2]-0.5]) for p in np.random.rand(npt,3)])
inTet_Nico=np.zeros(shape=(npt,1),dtype=bool)
inTet_Hugues=np.copy(inTet_Nico)
inTet_Dorian=np.copy(inTet_Nico)
inTet_Philipp=np.copy(inTet_Nico)
print("non vectorized, n points, different tetrahedrons:")
start_time = time.time()
for i in range(0,npt):
inTet_Nico[i]=pointInside_Nico(A[i,:],B[i,:],C[i,:],D[i,:],Pt[i,:])
print("Nico's: --- %s seconds ---" % (time.time() - start_time)) # https://stackoverflow.com/questions/1557571/how-do-i-get-time-of-a-python-programs-execution
start_time = time.time()
for i in range(0,npt):
inTet_Hugues[i]=pointInside_Hugues(A[i,:],B[i,:],C[i,:],D[i,:],Pt[i,:])
print("Hugues': --- %s seconds ---" % (time.time() - start_time))
start_time = time.time()
for i in range(0,npt):
inTet_Dorian[i]=pointInside_Dorian(A[i,:],B[i,:],C[i,:],D[i,:],Pt[i,:])
print("Dorian's: --- %s seconds ---" % (time.time() - start_time))
start_time = time.time()
for i in range(0,npt):
inTet_Philipp[i]=pointInside_Philipp(A[i,:],B[i,:],C[i,:],D[i,:],Pt[i,:])
print("Philipp's:--- %s seconds ---" % (time.time() - start_time))
print("vectorized, n points, 1 tetrahedron:")
start_time = time.time()
vertices = [A[0], B[0], C[0], D[0]]
tetra, origin = Tetrahedron(vertices)
inTet_Tom = pointInside(Pt, tetra, origin)
print("TomNorway's: --- %s seconds ---" % (time.time() - start_time))
for i in range(0,npt):
assert inTet_Hugues[i] == inTet_Nico[i]
assert inTet_Dorian[i] == inTet_Hugues[i]
#assert inTet_Tom[i] == inTet_Dorian[i] can not compare because Tom implements 1 tetra instead of n
assert inTet_Philipp[i] == inTet_Dorian[i]
'''errors = 0
for i in range(0,npt):
if ( inTet_Philipp[i] != inTet_Dorian[i]):
errors = errors + 1
print("errors " + str(errors))'''
Results:
non vectorized, n points, different tetrahedrons:
Nico's: --- 25.439453125 seconds ---
Hugues': --- 28.724457263946533 seconds ---
Dorian's: --- 15.006574153900146 seconds ---
Philipp's:--- 4.389788389205933 seconds ---
vectorized, n points, 1 tetrahedron:
TomNorway's: --- 0.008165121078491211 seconds ---
I've set up a demo with simple 3d first-person demo using C++ and OpenGL, and it seems to work reasonably well. My goal is this: when the user points the camera at a plane and clicks the left mouse button, I want to draw the intersection of a ray pointing in the direction the camera is facing from the player's position with that plane.
So, I start off with two Vectors, Vector position and Vector rotation, where Vector is a pretty standard three-dimensional vector class:
class Vector
{
public:
GLfloat x, y, z;
Vector() {};
Vector(GLfloat x, GLfloat y, GLfloat z)
{
this->x = x;
this->y = y;
this->z = z;
}
GLfloat dot(const Vector &vector) const
{
return x * vector.x + y * vector.y + z * vector.z;
}
... etc ...
And Plane p, with Plane being a simple struct storing the normal of the plane and d. I copied this struct directly from the book "Real-Time Collision Detection," by Christer Ericson:
struct Plane
{
Vector n; // Plane normal. Points x on the plane satisfy Dot(n,x) = d
float d; // d = dot(n,p) for a given point p on the plane
};
To start, I take position as the start of the ray, which I call a. I use that point and rotation to find the end of the ray, b. Then I use an algorithm for finding the intersection of a ray and a plane from that same book. I've actually implemented the same method myself, but I'm using the code from the book directly here just to make sure I didn't mess anything up:
void pickPoint()
{
const float length = 100.0f;
// Points a and b
Vector a = State::position;
Vector b = a;
// Find point b of directed line ab
Vector radians(Math::rad(State::rotation.x), Math::rad(State::rotation.y), 0);
const float lengthYZ = Math::cos(radians.x) * length;
b.y -= Math::sin(radians.x) * length;
b.x += Math::sin(radians.y) * lengthYZ;
b.z -= Math::cos(radians.y) * lengthYZ;
// Compute the t value for the directed line ab intersecting the plane
Vector ab = b - a;
GLfloat t = (p.d - p.n.dot(a)) / p.n.dot(ab);
printf("Plane normal: %f, %f, %f\n", p.n.x, p.n.y, p.n.z);
printf("Plane value d: %f\n", p.d);
printf("Rotation (degrees): %f, %f, %f\n", State::rotation.x, State::rotation.y, State::rotation.z);
printf("Rotation (radians): %f, %f, %f\n", radians.x, radians.y, radians.z);
printf("Point a: %f, %f, %f\n", a.x, a.y, a.z);
printf("Point b: %f, %f, %f\n", b.x, b.y, b.z);
printf("Expected length of ray: %f\n", length);
printf("Actual length of ray: %f\n", ab.length());
printf("Value t: %f\n", t);
// If t in [0..1] compute and return intersection point
if(t >= 0.0f && t <= 1.0f)
{
point = a + t * ab;
printf("Intersection: %f, %f, %f\n", point.x, point.y, point.z);
}
// Else no intersection
else
{
printf("No intersection found\n");
}
printf("\n\n");
}
When I render this point with OpenGL, it looks to be pretty close to the where the intersection of the ray and the plane would be. But from printing out the actual values, I discovered that for specific positions and rotations, the intersection point can be off by up to 0.000004. Here's an example of where the intersection is inaccurate - I know the intersection point is NOT on the plane because its Y value should be 0, not 0.000002. I could also sub it back into the plane equation and get an inequality:
Plane normal: 0.000000, 1.000000, 0.000000
Plane value d: 0.000000
Rotation (degrees): 70.100044, 1.899823, 0.000000
Rotation (radians): 1.223477, 0.033158, 0.000000
Point a: 20.818802, 27.240383, 15.124892
Point b: 21.947229, -66.788452, -18.894285
Expected length of ray: 100.000000
Actual length of ray: 100.000000
Value t: 0.289702
Intersection: 21.145710, 0.000002, 5.269455
Now, I know floating-point numbers are just approximations of real numbers, so I'm guessing this inaccuracy is just the effect of floating-point rounding, though it's possible I made a mistake somewhere else in the code. I know the intersection is off only by an extremely small amount, but I still care about it because I'm planning to use these points to define vertices of a model or level by snapping them to an arbitrarily-oriented grid, so I actually want those points to be ON that grid, even if they're slightly inaccurate. This might be a misguided approach - I don't really know.
So my question is: is this inaccuracy just floating-point rounding at work, or did I make a mistake somewhere else?
If it is just floating-point rounding, is there any way to deal with it? I've tried rounding the values of the rotation and position vectors in various ways, which obviously results in a less accurate intersection point, but I still sometimes get intersections that aren't on the plane. I did read an answer to a similar question (Is this plane-ray intersection code correct?) that mentions keeping the dimensions large, but I'm not sure exactly what that means.
Sorry if this question has been asked before - I searched, but I didn't see anything that was quite what I'm having trouble with. Thanks!
Your math seems correct and this definitely looks like a rounding error. I have a strong feeling that it is this line:
GLfloat t = (p.d - p.n.dot(a)) / p.n.dot(ab);
That said I don't see any other method to compute t. You could maybe verify if you are losing precision by using "%.12f" (or more) in your printf statements. Another way to pinpoint the culprit is to try doing your t computation step by step and printing the results along the way to see if you are losing precision somewhere.
Did you try using double precision floating point, if precision really matters to you that much?